Для начала я приведу небольшой тестовый проект из трёх классов, проанализирую его покрытие с помощью гема 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
Voila! Сгенерировался файл отчёт 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%-е покрытие не обязательно говорит о том, что тестов достаточно.
Материалы, используемые в статье:
Комментарии (6)
vanburg
16.12.2016 05:53Одна из сильных сторон руби как раз однострочники, и я не знаю ни одного способа заставить разрабов раздувать LOC, при этом еще и ухудшая как покрытие так и читаемость. Плюс это отличный способ обмануть CI и закомитить покрытие только одной ветки.
С покрытием как таковым вообще все сложно, даже в плане теории, а с такой проблемной реализацией, все становится еще сложнее.
IDMan
Спасибо за статью. Вопрос: в отчете покрытия не учтено проверку условия в методе walk_permitted, хотя чтобы добраться до выбора, нужно ведь отработать if. Это особенность работы или баг?
HedgeSky
Думаю, что в этом случае Coverage «склеивает» 23-ю и 24-ю строки, т.к. 23-я не была закончена. Вот эквивалентный вариант:
Считаю это более-менее корректным поведением.
IDMan
Кстати да, так перенос строки не влияет на конечный показатель. Хотя визуально все же немножко запутывает.
HedgeSky
До вашего комментария не замечал того, что перенесённые строки не подсвечиваются. Всегда ориентировался на красные строки в отчётах)
Спасибо!
IDMan
С другой стороны, если представить вариант подсветки этой строчки, возникнет вполне резонный вопрос «строчек 17, зелёных 16, красная одна, почему же в отчёте числа меньше, и куда вы подевали строку, я подсчитал вручную, вы врете, везде обман, уйду от вас». Согласен, что текущий вариант наименее конфликтен.