Для начала я приведу небольшой тестовый проект из трёх классов, проанализирую его покрытие с помощью гема 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)


  1. IDMan
    13.12.2016 23:39
    +1

    Спасибо за статью. Вопрос: в отчете покрытия не учтено проверку условия в методе walk_permitted, хотя чтобы добраться до выбора, нужно ведь отработать if. Это особенность работы или баг?


    1. HedgeSky
      14.12.2016 00:20
      +1

      Думаю, что в этом случае Coverage «склеивает» 23-ю и 24-ю строки, т.к. 23-я не была закончена. Вот эквивалентный вариант:



      Считаю это более-менее корректным поведением.


      1. IDMan
        14.12.2016 01:16
        +1

        Кстати да, так перенос строки не влияет на конечный показатель. Хотя визуально все же немножко запутывает.


        1. HedgeSky
          14.12.2016 01:21

          До вашего комментария не замечал того, что перенесённые строки не подсвечиваются. Всегда ориентировался на красные строки в отчётах)
          Спасибо!


      1. IDMan
        14.12.2016 01:27
        +1

        С другой стороны, если представить вариант подсветки этой строчки, возникнет вполне резонный вопрос «строчек 17, зелёных 16, красная одна, почему же в отчёте числа меньше, и куда вы подевали строку, я подсчитал вручную, вы врете, везде обман, уйду от вас». Согласен, что текущий вариант наименее конфликтен.


  1. vanburg
    16.12.2016 05:53

    Одна из сильных сторон руби как раз однострочники, и я не знаю ни одного способа заставить разрабов раздувать LOC, при этом еще и ухудшая как покрытие так и читаемость. Плюс это отличный способ обмануть CI и закомитить покрытие только одной ветки.
    С покрытием как таковым вообще все сложно, даже в плане теории, а с такой проблемной реализацией, все становится еще сложнее.