【问题标题】:How to test that an element is randomly selected from a list?如何测试一个元素是从列表中随机选择的?
【发布时间】:2016-04-12 17:27:51
【问题描述】:

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

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

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

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

【问题讨论】:

  • 文件是否是您正在测试的系统的一部分(例如,随附),它是否包含已知内容?还是由用户提供并可能包含任何内容?

标签: ruby unit-testing random rspec tdd


【解决方案1】:

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

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

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

我们可以编写以下代码,一次编写一个测试,使其通过并在编写下一个测试之前进行重构。以下是编写第三个测试之后但尚未实施之前的测试和代码:

spec/models/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

app/models/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

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

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

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

【讨论】:

    【解决方案2】:

    这里有一些关于如何测试这种东西的想法。对于像这样的简单功能,我可能不会全部使用它们,但对于更复杂的情况,它们仍然是很好的技术:

    1. 将条目读取到数组中的工作和从数组中选择随机元素的工作分开。

    2. 如果您不想在测试中依赖文件系统,您可以编写您的方法以使用一般的 IO 对象(或其他语言的流),然后使用 StringIO in你的测试。您将拥有一个简单的转发函数,它可以打开文件并将打开的文件传递给与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'
      

    注意:在最后一次测试中,我实际上不会有两个期望;我只是将它包含在内,以展示如何返回一系列随机值。

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

    【讨论】:

      【解决方案3】:

      看起来您走在正确的轨道上。我还会从数组中另外包含一个假的“样本”,并期望数组包含该样本。

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

      【讨论】:

      • 嗯,我正在尝试测试 subject.random_select 方法。我不太关心数组包含什么,所以我不确定不包含假样本的数组是否会为这个测试套件增加价值。
      猜你喜欢
      • 2023-01-11
      • 2018-03-06
      • 2011-05-29
      • 2014-02-07
      • 2017-04-26
      • 1970-01-01
      • 2017-05-11
      • 1970-01-01
      • 2013-10-19
      相关资源
      最近更新 更多