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

в 9:11, , рубрики: coverage, ruby, tdd, Программирование, Разработка веб-сайтов, Тестирование IT-систем

Для начала я приведу небольшой тестовый проект из трёх классов, проанализирую его покрытие с помощью гема SimpleCov, а напоследок немного поразмышляю о том, как анализ покрытия может приносить пользу проекту, и какие есть недостатки у Coverage в Ruby.

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

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

# Мама очень заботится о своём сыне, и не разрешает ему гулять,
# если он не надел шарф. А ещё она заботится о его успеваемости, поэтому если
# сын не сделал домашнюю работу, гулять ему она тоже не разрешит.
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. Посмотреть его можно по ссылке, а здесь я оставлю пару скриншотов, чтобы далеко не ходить (общий отчёт используется в качестве заглавной картинки).

Анализ покрытия кода тестами в Ruby - 2
father.rb

Анализ покрытия кода тестами в Ruby - 3
Выдержка из 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js