Для начала я приведу небольшой тестовый проект из трёх классов, проанализирую его покрытие с помощью гема SimpleCov, а напоследок немного поразмышляю о том, как анализ покрытия может приносить пользу проекту, и какие есть недостатки у Coverage в Ruby.
В качестве проекта для тестирования взята небольшая история о мальчике, который может спрашивать разрешения погулять у матери и у отца.
# Мама очень заботится о своём сыне, и не разрешает ему гулять,
# если он не надел шарф. А ещё она заботится о его успеваемости, поэтому если
# сын не сделал домашнюю работу, гулять ему она тоже не разрешит.
class Mother
def permit_walk?(child)
child.scarf_put_on && child.homework_done
end
end
# Отец тоже следит за тем, чтобы шарф был надет, но не так трепетно относится к учёбе.
class Father
def permit_walk?(child)
child.scarf_put_on
end
end
# Сын любит и уважает родителей, поэтому никогда не уходит гулять,
# не спросив разрешения. Спрашивать он может и у мамы, и у папы.
# Ну и, конечно, он может одеваться и делать ДЗ.
class Child
attr_reader :homework_done, :scarf_put_on
def initialize(mother, father)
@mother = mother
@father = father
@homework_done = false
@scarf_put_on = false
end
def do_homework!
@homework_done = true
end
def put_on_scarf!
@scarf_put_on = true
end
def walk_permitted?(whom_to_ask)
parent =
if whom_to_ask == :mother
@mother
else
@father
end
parent.permit_walk?(self)
end
end
Ну и потестируем немного (тесты намеренно покрывают не все сценарии):
require "simplecov"
SimpleCov.start
require "rspec"
require_relative "../lib/mother"
require_relative "../lib/father"
require_relative "../lib/child"
RSpec.describe Child do
let(:child) { Child.new(Mother.new, Father.new) }
context "when asking mother without scarf and without homework" do
it "isn't permitted to walk" do
expect(
child.walk_permitted?(:mother)
).to be false
end
end
context "when asking mother with scarf and with homework" do
it "is permitted to walk" do
child.put_on_scarf!
child.do_homework!
expect(
child.walk_permitted?(:mother)
).to be true
end
end
end
SimpleCov — фактически монополист в области анализа покрытия в мире Ruby 1.9.3+. Он является удобной обёрткой над модулем Coverage из стандартной библиотеки.
Подключение сводится к двум строкам в начале файла с тестами, при этом важно, чтобы инициализация SimpleCov проводилась до подключения файлов проекта. Запускаем тесты:
rspec
Voilà! Сгенерировался файл отчёт coverage/index.html. Посмотреть его можно по ссылке, а здесь я оставлю пару скриншотов, чтобы далеко не ходить (общий отчёт используется в качестве заглавной картинки).
father.rb
Выдержка из child.rb
Бонусы от анализа coverage
Из отчёта сразу видно, что не протестирован путь, в котором разрешение спрашивается у отца. Отсюда очевидная польза от анализа покрытия: в условиях неприменения TDD отчёт может показать, что мы забыли что-то протестировать. Если же проект достался в наследство и нелёгкий путь тестирования только начинается, отчёт поможет решить, куда эффективнее всего направить силы.
Второе возможное применение — автоматическое обеспечение "качества" коммитов. CI-сервер может отбраковывать коммиты, которые приводят к снижению total coverage, резко снижая вероятность появления в репозитории непротестированного кода.
Что анализ покрытия не даёт
Во-первых, стопроцентное покрытие не обеспечивает отсутствие багов. Простой пример: если изменить класс Mother таким образом:
class Mother
def permit_walk?(child)
# child.scarf_put_on && child.homework_done
child.homework_done
end
end
покрытие класса останется 100%-ым, тесты будут по-прежнему зелёными, но логика будет очевидно неверной. Для автоматического определения "отсутствующих, но нужных" тестов можно использовать гем mutant. Я ещё не пробовал его в деле, но, судя по Readme и количеству звёзд на гитхабе, библиотека действительно полезна. Впрочем, это тема для отдельного поста, до которого я как-нибудь доберусь.
Во-вторых, в Ruby на данный момент возможен анализ покрытия только по строкам, branch- и condition-coverage не поддерживается. Имеется в виду, что в однострочниках вида
some_condition ? 1 : 2
some_condition || another_condition
return 1 if some_condition
есть точки ветвления, но даже если тесты пройдут только по одной возможной ветви исполнения, coverage покажет 100%. Был pull request в Ruby на эту тему, но от мейнтейнеров уже два года ничего не слышно. А жаль.
Послесловие
Я предпочитаю писать тесты сразу же после написания кода, и coverage служит мне напоминалкой о ещё не протестированных методах (частенько забываю потестить обработчики исключений). В общем, анализ покрытия вполне может приносить определённую пользу, но 100%-е покрытие не обязательно говорит о том, что тестов достаточно.
Материалы, используемые в статье:
Автор: HedgeSky