Анализ покрытия кода тестами в Ruby


В Ruby фактически монополистом в области анализа покрытия является гем SimpleCov, который является удобной обёрткой над модулем Coverage из стандартной библиотеки.

Для начала я приведу небольшой тестовый проект из трёх классов, проанализирую его покрытие, а напоследок немного поразмышляю о том, как анализ покрытия может приносить пользу проекту, и какие есть недостатки у 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 сводится к двум строкам в начале файла с тестами, при этом важно, чтобы инициализация SimpleCov проводилась до подключения файлов проекта. Запускаем тесты:

ruby tests/test_child.rb

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%-е покрытие не обязательно говорит о том, что тестов достаточно.

Материалы, используемые в статье:

ссылка на оригинал статьи https://habrahabr.ru/post/317326/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *