Тестирование кода и TDD
В этом уроке мы разберем концепцию тестирования нашего программного кода, научимся это делать на примере простейших библиотек, а также изучим подход в разработке через тестирование.
Тестирование
Давайте рассмотрим пример программы для подсчета факториала (Факториалом числа n называется произведение всех натуральных чисел от 1 до n включительно):
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
:
# Оставим 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
class CalculatorTest
end
Для того, чтобы возспользоваться библиотекой minitest
необходимо добавить зависимость в файл с тестовым файлом и наследоваться от специального класса Minitest::Test
следующим образом:
require "minitest/autorun"
class CalculatorTest < Minitest::Test
end
Далее нам неободимо создать методы, которые будут соотвествовать каждому нашему тестовому сценарию, которые мы определили ранее
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
Как можете заметить каждый метод имеет название тестируемого метода, а также описание тестового сценария.
Теперь нам необходимо создать наш объект, который будем тестировать и воспроизвести сценарии в каждом методе:
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
, так как в обоих ошибочных сценариях передан аргумент, который мы не ожидаем получить.
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
, значит тест не пройден. Вторым аргументом он принимает сообщение, которое будет показано в случае если тест не пройден. Этот аргумент опционален, поэтому передавать его необязательно. Посмотрим на примере ожидания корректного результата
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 аргумент для текстового описания на случай непройденного текста, просто держите это в уме)
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
, который принимает в качестве аргумента класс ошибки, а также блок кода, внутри которого мы должны вызывать наш метод
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
Помимо проверки типа ошибки, мы можем получить сам объект ошибки и проверить сообщение, которое находится внутри него.
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
вы сможете ознакомиться в допольнительных материалах. А пока давайте допишем наши оставшиеся тесты
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
для вашего класса
Last updated