如何测试从列表中随机选择的元素?

时间:2016-04-12 17:27:51

标签: ruby unit-testing random rspec tdd

我正在研究Rails应用程序并尝试练习TDD(使用RSpec)。我的lib目录中有一个文件,其中包含一个字符串列表,以及一个读取该文件并从列表中随机选择其中一个字符串的方法。我还没有实现这个方法,因为我正在努力学习如何编写测试功能。

有很多方法可以从数组中随机选择一个对象,并且在这里有很多很好的回答问题,例如this one,告诉我如何做到这一点(当涉及到实现时,我会可能使用Array#sample)。但我的期望是什么呢?我想的是:

expect(array).to include(subject.random_select)

这肯定会断言我的方法会返回一些预期的值 - 但这足以断言该方法每次都会随机返回一个不同的字符串吗?什么是替代方案,或者可能是确保我已经覆盖这种方法的其他测试?我真的不能指望subject.random_select等于伪造的输入,我可以吗?

3 个答案:

答案 0 :(得分:4)

我首先测试单行文件中单个字符串的非随机选择,然后我测试从多行文件中选择一个字符串,然后我测试选择是随机的。你无法在有限时间内真正测试随机性,所以你能做的最好就是

  • 测试您的方法返回所需范围内的值,因为您的测试将在应用程序的生命周期内运行很多次,您可能会发现它是否会返回超出范围的内容,并且
  • 证明您的代码使用了随机源。

假设您的测试环境中不存在该文件,或者您不知道其内容,或者不希望一次测试的不对称性和其他测试的不对称性,所以我们会需要提供一种测试方法,将类指向不同的文件。

我们可以编写以下内容,一次编写一个测试,在编写下一个测试之前通过并重构。以下是在编写第三个测试之后但在实现之前的测试和代码:

规格/模型/ thing_spec.rb

describe Thing do
  describe '.random_select' do
    it "returns a single line from a file with only one line" do
      allow(Thing).to receive(:file) { "spec/models/thing/1" }
      expect(Thing.random_select).to eq("Thing 1")
    end

    it "returns a single line from a file with multiple lines" do
      allow(Thing).to receive(:file) { "spec/models/thing/2" }
      expect(Thing.random_select).to be_in(['Thing 1', 'Thing 2'])
    end

    it "returns different lines at different times" do
      allow(Thing).to receive(:file) { "spec/models/thing/2" }
      srand 0
      thing1 = Thing.random_select
      srand 1
      thing2 = Thing.random_select
      expect(thing1).not_to eq(thing2)
    end

  end
end

应用程序/模型/ thing.rb

class Thing
  def self.random_select
    "Thing 1" # this made the first two tests pass, but it'll need to change for all three to pass
  end

  def self.file
    "lib/things"
  end

end

当我编写第二个测试时,我意识到它没有任何额外的代码更改,所以我考虑删除它。但我推迟了这个决定,编写了第三个测试,并发现一旦第三个测试通过第二个测试就会有价值,因为第二个测试测试的是值来自文件,但第三个测试没有。

be_in是一种更好的方法来测试返回值是否在include已知集合中,因为它将实际值放在RS {期望的expect内。

还有其他方法可以控制随机性,以便您可以测试它的使用情况。例如,如果您使用sample,则可以allow_any_instance_of(Array).to receive(:sample)并返回您喜欢的任何内容。但我喜欢使用srand,因为它不要求实现使用使用随机数生成器的特定方法。

如果文件丢失或为空,您还需要对其进行测试。

答案 1 :(得分:2)

以下是关于我如何测试此类内容的一些想法。我可能不会为这样一个简单的函数做所有这些,但它们仍然是更复杂案例的好技术:

  1. 将将条目读入数组的工作分开,以及从数组中选择随机元素的工作。

  2. 如果您不想在测试中依赖文件系统,可以编写方法来处理常规IO对象(或其他语言的流)和然后在测试中使用StringIO。您有一个简单的转发功能,可以打开文件并将打开的文件传递给使用IO的方法。例如:

    def read_entries(file)
      File.open(file) { |io| read_entries_from_io(io) }
    end
    
    def read_entries_from_io(io)
      # ... do the work ...
    end
    
    # In your spec:
    io = StringIO.new("Entry1\nEntry2\nEntry3\n")
    expect(read_entries_from_io(io)).to eq %w[Entry1 Entry2 Entry3]
    
  3. 几乎每个随机执行某些操作的Ruby方法(如sampleshuffle)都会使用一个可选的random:关键字参数,它允许您提供自己的随机数生成器。如果你的方法遵循相同的约定,那么你可以从你的测试中注入一个伪随机数生成器,它返回一个硬编码的伪随机数序列:

    def random_select(entries, random: Random.new)
      entries.sample(random: random)
    end
    
    # In the spec:
    
    entries = %w[Entry0 Entry1 Entry2 Entry3]
    random = instance_double(Random)
    allow(random).to receive(:rand).and_return(2, 0)
    
    expect(random_select(entries, random: random)).to eq 'Entry2'
    expect(random_select(entries, random: random)).to eq 'Entry0'
    
  4. 注意:在最后一次测试中,我实际上并没有两个期望值;我只是将它包含在内,以显示如何返回一系列随机值。

    此外,在这种情况下,测试正在假设sample如何使用随机数生成器。这可能适用于sample,但可能不适用于shuffle

答案 2 :(得分:0)

看起来你走在正确的轨道上。我还要包括一个假的"样本"从数组中,并期望数组包含该样本。

See these docs,例如: expect(array).not_to include("fake sample")