шаблон проектирования rspec для тестирования нескольких точек данных - PullRequest
4 голосов
/ 20 ноября 2011

Я часто нахожу, что при написании тестов для метода я хочу добавить в метод кучу различных входных данных и просто проверить, соответствует ли результат ожидаемым.

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

Следующий код, кажется, справляется с этой задачей, но мне интересно, есть ли лучшая практика, которую я должен использовать (например, используя subject, context):

describe "my_square_function" do
  @tests = [{:input => 1, :result => 1},
            {:input => -1, :result => 1},
            {:input => 2, :result => 4},
            {:input => nil, :result => nil}]
  @tests.each do |test|
    it "squares #{test[:input].inspect} and gets #{test[:result].inspect}" do
      my_square_function(test[:input]).should == test[:result]
    end
  end
end

Предложения

Спасибо!

(Связано: Рефакторинг rspec? )

Ответы [ 2 ]

10 голосов
/ 23 ноября 2011

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

describe "my_square_function" do
  @tests = {1  => 1,
            -1  => 1,
            2  => 4,
            nil => nil}

  @tests.each do |input, expected|
    it "squares #{input} and gets #{expected}" do
      my_square_function(input).should == expected
    end
  end
end
5 голосов
/ 23 ноября 2011

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

Поскольку этот вопрос помечен как TDD, я предполагаю, что вы пишете стиль метода TDD. Если это так, вы можете начать с:

 describe "my_square_function" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end
 end

Имея провальный тест, вы можете реализовать my_square_function следующим образом:

 def my_square_function(number)
   1
 end

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

 def my_square_function(number)
   number
 end

Теперь, когда дублирование удалено и тесты все еще проходят, мы можем перейти к следующему тесту:

 describe "my_square_function" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end

   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end
 end

Запустив тесты, вы снова встретились с провальным тестом, поэтому мы сдаем его:

 def my_square_function(number)
   number.abs   # of course I probably wouldn't really do this but
                # hey, it's an example. :-)
 end

Теперь этот тест пройден, и пришло время перейти к другому тесту:

 describe "my_square_function" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end

   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end

   it "Squares other positive numbers" do
     my_square_function(2).should == 4
   end
 end

На этом этапе ваш новый тест больше не будет проходить, поэтому теперь для его прохождения:

 def my_square_function(number)
   number.abs * number
 end

К сожалению. Это не совсем сработало, это вызвало сбой теста с отрицательным числом. К счастью, неудача подтолкнула нас к точному тесту, который не сработал, мы знаем, что он провалился из-за «негативного» теста. Вернуться к коду:

 def my_square_function(number)
   number.abs * number.abs
 end

Это лучше, все наши тесты пройдены. Пришло время рефакторинга снова. Здесь мы видим другой ненужный код в этих вызовах abs. Мы можем избавиться от них:

 def my_square_function(number)
   number * number
 end

Тесты все еще проходят, и мы видим еще одно дублирование с этим противным аргументом. Посмотрим, сможем ли мы от этого избавиться:

 def my_square_function(number)
   number ** 2
 end

Тест пройден, и у нас больше нет этого дублирования. Теперь, когда у нас есть чистая реализация, давайте разберем случай nil:

 describe "my_square_function" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end

   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end

   it "Squares other positive numbers" do
     my_square_function(2).should == 4
   end

   it "Doesn't try to process 'nil' arguments" do
     my_square_function(nil).should == nil
   end
 end

Хорошо, мы снова вернулись к неудаче, и мы можем продолжить и реализовать проверку nil:

 def my_square_function(number)
   number ** 2 unless number == nil
 end

Этот тест пройден, и он довольно чистый, поэтому мы оставим все как есть. Теперь вернемся к спецификации и посмотрим, что у нас есть, и убедимся, что нам нравится то, что мы видим:

 describe "my_square_function" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end

   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end

   it "Squares other positive numbers" do
     my_square_function(2).should == 4
   end

   it "Doesn't try to process 'nil' arguments" do
     my_square_function(nil).should == nil
   end
 end

Мое первое желание состоит в том, что мы действительно описываем поведение «возведения в квадрат» числа, а не самой функции, поэтому мы изменим это:

 describe "How to square a number" do
   it "Squares a positive number" do
     my_square_function(1).should == 1
   end

   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end

   it "Squares other positive numbers" do
     my_square_function(2).should == 4
   end

   it "Doesn't try to process 'nil' arguments" do
     my_square_function(nil).should == nil
   end
 end

Теперь, три примера названия немного мягкие, если их поместить в этот контекст. Я собираюсь начать с первого примера. Кажется, это немного глупо для квадрата 1. Это выбор, который я собираюсь сделать, чтобы уменьшить количество примеров в коде. Я действительно хочу, чтобы примеры были интересными, иначе я не буду их тестировать. Разница между квадратом 1 и 2 неинтересна, поэтому я удалю первый пример. Сначала это было полезно, но не дольше. Это оставляет нас с:

 describe "How to square a number" do
   it "Squares a negative number" do
     my_square_function(-1).should == 1
   end

   it "Squares other positive numbers" do
     my_square_function(2).should == 4
   end

   it "Doesn't try to process 'nil' arguments" do
     my_square_function(nil).should == nil
   end
 end

Следующая вещь, которую я собираюсь рассмотреть, - это негативный пример, связанный с контекстом в блоке description. Я собираюсь дать ему и остальным примерам новые описания:

 describe "How to square a number" do
   it "Squaring a number is simply the number multiplied by itself" do
     my_square_function(2).should == 4
   end

   it "The square of a negative number is positive" do
     my_square_function(-1).should == 1
   end

   it "It is not possible to square a 'nil' value" do
     my_square_function(nil).should == nil
   end
 end

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

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

Надеюсь, это поможет!

Brandon

...