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

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


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


    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 6

      +1
      Спасибо за статью. Вопрос: в отчете покрытия не учтено проверку условия в методе walk_permitted, хотя чтобы добраться до выбора, нужно ведь отработать if. Это особенность работы или баг?
        +1
        Думаю, что в этом случае Coverage «склеивает» 23-ю и 24-ю строки, т.к. 23-я не была закончена. Вот эквивалентный вариант:



        Считаю это более-менее корректным поведением.
          +1
          Кстати да, так перенос строки не влияет на конечный показатель. Хотя визуально все же немножко запутывает.
            0
            До вашего комментария не замечал того, что перенесённые строки не подсвечиваются. Всегда ориентировался на красные строки в отчётах)
            Спасибо!
            +1
            С другой стороны, если представить вариант подсветки этой строчки, возникнет вполне резонный вопрос «строчек 17, зелёных 16, красная одна, почему же в отчёте числа меньше, и куда вы подевали строку, я подсчитал вручную, вы врете, везде обман, уйду от вас». Согласен, что текущий вариант наименее конфликтен.
          0
          Одна из сильных сторон руби как раз однострочники, и я не знаю ни одного способа заставить разрабов раздувать LOC, при этом еще и ухудшая как покрытие так и читаемость. Плюс это отличный способ обмануть CI и закомитить покрытие только одной ветки.
          С покрытием как таковым вообще все сложно, даже в плане теории, а с такой проблемной реализацией, все становится еще сложнее.

          Only users with full accounts can post comments. Log in, please.