如何使用 RSpec 编写循环遍历一系列值的测试?

How to write a test which loops through a range of values using RSpec?

我有一个非常简单的 Ruby 游戏实现,叫做 "FizzBuzz"(即给定一个输入数字,如果数字是 3 的倍数,它 returns "Fizz" , "Buzz" 如果是 5 的倍数,"FizzBuzz" 如果是两者的倍数,如果它不符合前面的任何条件,则为原始数字):

class FizzBuzz
    def answer(number)
        multiple3 = number%3 == 0
        multiple5 = number%5 == 0
        return case
        when (multiple3 and multiple5) then "FizzBuzz"
        when multiple3 then "Fizz"
        when multiple5 then "Buzz"
        else number
        end
    end
end

我使用 RSpec 编写了一个测试来验证每个条件:

require "rspec"
require "./fizzBuzz"

RSpec.describe "#answer" do
    it "returns Buzz when number is multiple of 3" do 
        result = FizzBuzz.new.answer(3)
        expect(result).to eq("Fizz")
    end

    it "returns Buzz when number is multiple of 5" do 
        result = FizzBuzz.new.answer(5)
        expect(result).to eq("Buzz")
    end

    it "returns a number when the input number is neither multiple of 3 nor 5" do 
        result = FizzBuzz.new.answer(11)
        expect(result).to eq(11)
    end

end

该测试完美运行,但是,我在其中使用了具体值(即 3、5 和 11)。

我的问题是:如果我想使用广泛的值(例如从 1 到 10000)测试我的 FizzBu​​zz Ruby 脚本怎么办?

我知道我可以通过直接在 RSpec 中使用每个循环和案例来解决这个问题,但是,我担心的是如果在我的测试中我采用与 Ruby 脚本中相同的条件语句待测试(即 when number%3 == 0 then "Fizz" 等)我将使用 RSpec 脚本测试我的代码,该脚本遵循与要测试的脚本完全相同的逻辑,因此测试可能会成功通过。

还有什么选择?是否有使用大量值(例如使用循环)而不是硬编码或特定值来编写测试的最佳实践?

这里的一个可能 half-way 点是循环遍历 RSpec 测试中的可能答案。保持代码干燥很重要,但保持测试干燥也很重要,有时 under-estimated.

这样的事情怎么样:

  RSpec.describe "#answer" do
      expected_values = {'3': 'Fizz', '5': 'Buzz', '6': 'Fizz', '11': '11', '15': 'FizzBuzz'}
      expected_values.each do |val, expected| 
        it "returns #{expected} when number is #{val}" do 
            result = FizzBuzz.new.answer(val.to_i)
            expect(result).to eq(expected)
        end
      end  
    end

这样,您可以通过将测试添加到 expected_values 散列来轻松添加测试,但是如果方法名称更改或类似的东西,您只需更改一个地方

我会假装你在问,以防你真的需要它,这个简单的例子只是为了说明。

有 属性 Based Testing 可以优雅地解决此类问题,使用这种方法,您必须找到一些 属性(例如,将两个数字相加结果优于两个数字和 a + b = b + a ...)。您将使用一个框架,该框架将随机生成条目,以便覆盖比特定情况下更大的范围。

在 Ruby 中也有可以提供帮助的框架。

属性 基于测试的出色解释http://fsharpforfunandprofit.com/posts/property-based-testing/(免责声明代码在 Fsharp 中)

您可以在规格中使用循环:

RSpec.describe "#answer" do

  RESULTS = { 3 => "Fizz",
              5 => "Buzz",
              11 => 11 }

  RESULTS.each do |value, answer|
    it "returns #{answer} when the input is #{value}" do 
      result = FizzBuzz.new.answer(value)
      expect(result).to eq(answer)
    end
  end

end

RSpec 现在有 built-in 很酷的功能,比如共享示例。更多信息 here.

结果我们会得到这样的共享组(注意,这个组可以用于其他测试):

shared_examples "shared example" do |number, result|
  it "returns #{result} when accepts #{number}" do
    # implicit `subject` here is equal to FizzBuzz.new
    expect(subject.answer(number)).to eq(result)
  end
end

并且测试将更具可读性并以更多"rspec-like"方式编写:

DATA_SET = {15 => 'FizzBuzz', 3 => 'Fizz', 5 => 'Buzz', 11 => 11}

RSpec.describe FizzBuzz do
  context "#answer" do
    DATA_SET.each do |number, result|
      # pseudo-randomizing test data.
      number = [1,2,4,7,8,11,13,14,16].sample*number
      result = number if result.is_a?(Integer)
      include_examples "shared example", number, result
    end
  end
end

为了支持已接受的答案,我建议将更广泛的测试包装在上下文块中,以便测试与其他代码保持隔离:

RSpec.describe "#answer" do

  context 'when testing inputs and answers' do

    RESULTS = { 3 => "Fizz",
                5 => "Buzz",
                11 => 11 }

    RESULTS.each do |value, answer|
      it "returns #{answer} when the input is #{value}" do 
        result = FizzBuzz.new.answer(value)
        expect(result).to eq(answer)
      end
    end

  end

end