Как вы четко показываете намерение при тестировании функций, принимающих блоки в RSpec?

#ruby #rspec #bdd

#ruby #rspec #bdd

Вопрос:

Я просто вхожу в RSpec и немного поиграю с некоторыми простыми примерами и реализую древовидную структуру узлов с доступными для посещения узлами.

Первый тест, который я использовал для очистки кода с помощью bdd, был:

 describe "Tree" do
  it "is visitable" do
    t = Tree.new
    visited = nil
    t.visit { |n| visited = n }
    visited.should == t
  end
end
  

Это дает мне следующую реализацию:

 class Tree
  def visit(amp;block)
    block.call self
  end
end
  

На данный момент я не слишком доволен кодом RSpec, поскольку на самом деле он не очень четко показывает намерение того, что я пытаюсь сделать, хотя технически это работает. Когда я перехожу к реализации дочерних элементов, это становится еще более запутанным:

 it "has visitable children" do
  c1, c2 = Tree.new, Tree.new
  t = Tree.new([c1, c2])
  visited = Set.new

  t.visit { |n| visited.add(n) }

  visited.should == Set.new([t, c1, c2])
end
  

Это дает мне полную реализацию:

 class Tree
  attr_accessor :children
  def initialize(children=[])
    @children = children
  end

  def visit(amp;block)
    block.call self

    children.each { |c| c.visit amp;block }
  end
end
  

Я достаточно доволен результирующей реализацией (являющейся ознакомительным примером и всем прочим), но есть ли идиома RSpec, которая может сделать спецификацию более продуманной и легко читаемой?

Редактировать: Чтобы уточнить, мне интересно, есть ли хорошие способы справиться с этим с помощью помощников RSpec / mocks и т.д.

Комментарии:

1. Вы имеете в виду block.call self в первой версии Tree#visit ?

Ответ №1:

Я бы сказал, что лучше, если тесты также будут выглядеть правильно. Реальный вопрос в том, зачем вообще использовать TDD? Определить ваши варианты использования (вообще говоря) перед написанием кода и определить API так, чтобы он соответствовал вашим вариантам использования. По крайней мере, таков мой опыт: используя TDD с RSpec или что-то еще, я в конечном итоге определяю лучшие интерфейсы для своего кода. Это потому, что необходимость протестировать как можно больше функциональности требует сократить API таким образом, чтобы обеспечить легкий доступ к отдельным частям (и макет, если необходимо).

Итак, на самом деле я ожидал бы, что тестовые примеры будут выглядеть как производственный код: потому что они будут содержать точно такой же API, что и производственный код. Если в итоге получается беспорядок, то это может означать только то, что вы еще не готовы к своему API?

Использование mocks тоже помогает, потому что оно выражает ваши ожидания, как в

 @target.should_receive(:print).exactly(1).times
  

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

 describe "Tree" do

  it "is visitable" do
    t = Tree.new
    block = lambda { |n| n }
    block.should_receive(:call).with(t).exactly(1).times
    t.visit amp;block
  end

  it "has visitable children" do
    c1, c2 = Tree.new, Tree.new
    t = Tree.new([c1, c2])

    block = lambda { |n| n }
    block.should_receive(:call).with(kind_of(Tree)).exactly(3).times { |n|
      [t, c1, c2].include?(n)
    }

    t.visit amp;block
  end
end
  

Комментарии:

1. Итак, как бы вы изменили пример в вопросе, чтобы с пользой использовать mocks?

Ответ №2:

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

Тем не менее, вы могли бы немного почистить примеры, поместив object setup в блок before (:each) и / или разделить ваши примеры на контексты для деревьев с одним узлом и деревьев с дочерними элементами. Что-то вроде этого:

     context "singleton trees" do
      before(:each) do
        @tree1 = Tree.new
        @tree2 = Tree.new
      end

      ...

      context "trees with children" do

        before(:each) do
          @tree_with_children = Tree.new([@tree1, @tree2)
        end     

       ...

      end
    end
  

Комментарии:

1. Спасибо, это определенно полезно. Я попытался добавить некоторые пояснения в вопрос, чтобы показать, что я ищу новые методы для изучения в RSpec. Имея неудачный опыт работы с неуправляемым тестовым кодом и, возможно, наивное представление о том, что большую часть кода можно сделать легко понятной, я еще не готов отказаться от того, чтобы сделать его более понятным.