> For the complete documentation index, see [llms.txt](https://evgenii-afanasev.gitbook.io/ruby-course/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://evgenii-afanasev.gitbook.io/ruby-course/8_libraries/3_testing.md).

# Тестирование кода и TDD

В этом уроке мы разберем концепцию тестирования нашего программного кода, научимся это делать на примере простейших библиотек, а также изучим подход в разработке через тестирование.

## Тестирование

Давайте рассмотрим пример программы для подсчета факториала (Факториалом числа n называется произведение всех натуральных чисел от 1 до n включительно):

```ruby
class Calculator
  def factorial(max)
    sum = 1
    1.upto max do |num|
      sum *= num
    end
    sum
  end
end
```

Что будет есть мы передадим в качестве аргумента nil ? Да, мы можем протестировать нашу программу вручную, может обработать этот случай, но есть ли у нас хоть какие-то гарантии, что через несколько обновлений эта программа будет стабильно продолжать работать ? Если мы будем самостоятельно тестировать каждое обновление нашей программы, то это будет занимать колосальное кол-во времени.

Для решения этой проблемы существует тестирование нашей программы прямо внутри нашего кода. Это напоминает наш `main.rb` файл, в котором мы вызывали методы нашего объекта и смотрели, что получилось, но здесь мы уже задействуем специальные библиотеки, которые делают наше тестирование более прозрачным, структурированным.

Для ознакомления с этим механизмом первым делом необходимо установить библиотеку. На данный ммомент наиболее популярными являются `minitest` и `rspec`. В рамках данного урока мы познакмимся с `minitest` за счет ее простоты для первоначального знакомства, в дальнейшем при знакомстве с Rails мы изучим `rspec`.

Начнем с установки `minitest` через `bundler`. Добавьте библиотеку в `Gemfile`:

```ruby
# Оставим nokogiri для сохранения консистентности
gem 'nokogiri', '~> 1.14'
# Добавляем minitest
gem 'minitest', '~> 5.18'
```

После чего произведем установку с помощью `bundle install`. Готово, теперь мы можем начать пользоваться нашей библиотекой.

Юнит тесты тестируют не всю нашу программу, а только ее часть. В ООП парадигме блоками нашей программы являются классы, соответственно мы тестируем наши классы. Один класс - один тестовый класс. Тестовые классы следует называть точно также как и тестируемые классы с добавлением приписки `Test`, Допустим наш класс называется `BookLibrary`, значит тестовый класс будет называться `BookLibraryTest`. Кроме того, тестовые классы и классы с бизнес-логикой необходимо разделять между собой. Для этого можно создавать в директории продекта новый каталог `tests`, который будет содержать наши тетсовые классы.

Для начала давайте подумаем как все возможные сценария использования вашей программы, которые возможны:

* передано целое положительное число
* передано число с плавающей запятой
* передан 0
* передано отрицательное число

Отлично ! Теперь может приступить к тестированию. Создаем каталог, который будет содержать тесты. У меня класс `Calculator` лежит по пути `8_libraries/3_testing/calculator.rb`, значит я создам каталог с тестами в месте `8_libraries/3_testing/tests` с названием файла `calculator.rb` и названием класса `CalculatorTest`

```ruby
class CalculatorTest
end
```

Для того, чтобы возспользоваться библиотекой `minitest` необходимо добавить зависимость в файл с тестовым файлом и наследоваться от специального класса `Minitest::Test` следующим образом:

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
end
```

Далее нам неободимо создать методы, которые будут соотвествовать каждому нашему тестовому сценарию, которые мы определили ранее

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 

  def factorial_pass_integer
  end

  def factorial_pass_double
  end

  def factorial_pass_zero
  end

  def factorial_pass_negative
  end
end
```

Как можете заметить каждый метод имеет название тестируемого метода, а также описание тестового сценария.

Теперь нам необходимо создать наш объект, который будем тестировать и воспроизвести сценарии в каждом методе:

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 

  # Так как наш объект не хранит никакого состояния, то мы создаем его один раз
  # В случае если у объекта состояние есть, то следует его создавать в каждом методе заново
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    puts actual
  end

  def factorial_pass_double
    actual = calculator.factorial(4.2)
    puts actual
  end

  def factorial_pass_zero
    actual = calculator.factorial(0)
    puts actual
  end

  def factorial_pass_negative
    actual = calculator.factorial(-10)
    puts actual
  end
end
```

И в конце нам нужно определиться, какое поведение мы **ожидаем** от нашей программы. Наш код является безопасным в тот момент, когда мы получаем ожидаемое поведение в каждом случае. Давайте разберем все наши кейсы

* Если передано положительное число, то мы ожидаем получить корректное положительное число
* Число с плавающей запятой не может передоваться в факториал, поэтому самый лучший способ - ожидать ошибку.
* При передаче 0 мы должны получить в качестве результата 1
* Отрицательное число передавать в факториал мы тоже не может, поэтому тоже следует ожидать ошибку

Изучив наши сценарии и ожидаемый ответ, давайте исправим нашу программу.

Ранее мы изучали тему выброса ошибок. Вместо того, чтобы создавать свои собственные ошибки, можно воспользоваться уже существующими классами в Ruby. Например, в наших сценариях лкчше всего по смыслу подойдет ошибка `ArgumentError`, так как в обоих ошибочных сценариях передан аргумент, который мы не ожидаем получить.

```ruby
class Calculator
  def factorial(max)
    if max.is_a? Float
      raise ArgumentError.new "value can't be float"
    elsif max < 0
      raise ArgumentError.new "value can't be negative"
    else
      sum = 1
      1.upto max do |num|
        sum *= num
      end
      sum
    end
  end
end
```

Теперь когда наша программа должна отрабатывать корректно, убедимся в этом с помощью тестов.

В `minitest` для различных проверок существуют специальные методы **Сравнения**. обычно все эти методы начинаются с `assert`.

Самый базовый метод `assert` принимает в качестве первого аргумента какой-то `boolean`. Если он равен `true`, значит этот тест пройден, если `false`, значит тест не пройден. Вторым аргументом он принимает сообщение, которое будет показано в случае если тест не пройден. Этот аргумент опционален, поэтому передавать его необязательно. Посмотрим на примере ожидания корректного результата

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    # первый аргумент это bollean, второй - сообщение, если результат первого аргумента будет false
    assert(actual == 120, 'factorial return not corrent result')
  end

  # остальные тесты
  # ...
end
```

В целом почти любую проверку можно выразить с помощью `true/false`, но `minitest` нам предоставляет дополнительные методы-обертки для более удобных и понятных проверок.

Например сравнение можно заменить на `assert_equal`, который первым аргументом принимает ожидаемое значение, а вторым реально полученное (на самом деле всегда есть + 1 аргумент для текстового описания на случай непройденного текста, просто держите это в уме)

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    assert_equal(120, actual, 'factorial return not corrent result')
  end

  # остальные тесты
  # ...
end
```

Помимо сравнения результата наш код может выбрасывать ошибки, которые нам необходимо тоже проверять. Можно и свои костыли написать через `begin...end`, но будет выглядеть это, мягко говоря, не очень красиво. Для этого `minitest` предлагает нам специальный метод для проверки ошибки `assert_raises`, который принимает в качестве аргумента класс ошибки, а также блок кода, внутри которого мы должны вызывать наш метод

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    assert_equal(120, actuald)
  end

  def factorial_pass_double
    # Первый аргумент - класс ошибки, а в бдлоке мы вызываем метод, который должен эту ошибку выбросить
    assert_raises(ArgumentError) do
      calculator.factorial(4.2)
    end
  end

  # остальные тесты
  # ...
end
```

Помимо проверки типа ошибки, мы можем получить сам объект ошибки и проверить сообщение, которое находится внутри него.

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    assert_equal(120, actual)
  end

  def factorial_pass_double
    # Метод в качестве результата возвращает объект ошибки
    err = assert_raises(ArgumentError) do
      calculator.factorial(4.2)
    end
    assert_equal("value can't be float", err.message)
  end

  # остальные тесты
  # ...
end
```

Более подробно с различными вариантами метода `assert` вы сможете ознакомиться в допольнительных материалах. А пока давайте допишем наши оставшиеся тесты

```ruby
require "minitest/autorun"

class CalculatorTest < Minitest::Test 
  calcalator = Calculator.new

  def factorial_pass_integer
    actual = calculator.factorial(5)
    assert_equal(120, actual)
  end

  def factorial_pass_double
    err = assert_raises(ArgumentError) do
      calculator.factorial(4.2)
    end
    assert_equal("value can't be float", err.message)
  end

  def factorial_pass_zero
    actual = calculator.factorial(0)
    assert_equal(1, actual)
  end

  def factorial_pass_negative
    err = assert_raises(ArgumentError) do
      calculator.factorial(-10)
    end
    assert_equal("value can't be negative", actual)
  end
end
```

## TDD

В прошлых примерах мы сначала написали нашу программу, а уже потом обложили ее тестами. Существует методика, главный принцип которой - делать все наоборот. По TDD мы **сначала пишем тесты**, а уже после этого пишем нашу программу, чтобы она удовлетворяла условиям этих тестов. Данный способ написания программы считается наиболее безопасным, но с непривычки писать по такой методике достаточно тяжело.

## Задание string\_utils

* Напишите класс `StringUtils` и реализуйте в нем метод `camel_to_snake_case`, который принимает строку в формате `camelCase` и возвращает эту же строку в формате `snake_case`
* Составьте все возможные сценарии использования этого метода
* Напишите тесты с помощью `minitest` для вашего класса
