如何测试一个元素是从列表中随机选择的?
How to test that an element is randomly selected from a list?
我正在开发 Rails 应用程序并尝试练习 TDD(使用 RSpec)。我的 lib 目录中有一个文件,其中包含一个字符串列表,以及一个读取该文件并随机 select 列表中的一个字符串的方法。我还没有实现这个方法,因为我正在为如何编写测试该功能而苦苦挣扎。
有很多方法可以从数组中随机 select 一个对象,还有很多很好的回答问题,比如 this one 告诉我如何做到这一点(归结为实现,我可能会使用 Array#sample
)。但我的期望应该是什么?我在想:
expect(array).to include(subject.random_select)
这肯定会断言我的方法返回了一些预期值 — 但是否足以断言该方法每次随机 returns 一个不同的字符串?有哪些替代方法,或者额外的测试可以确保我已经覆盖了这种方法?我真的不能指望 subject.random_select
等于伪造的输入,可以吗?
看来您的方向是正确的。我还会从数组中包含一个伪造的 "sample",并期望数组 不 包含该样本。
See these docs,例如:
expect(array).not_to include("fake sample")
我会首先测试从单行文件中非随机选择单个字符串,然后测试从多行文件中选择字符串,然后我测试选择是随机的。你不能在有限的时间内真正测试随机性,所以你能做的最好的就是
- 测试您的方法 return 的值是否在所需范围内,因为您的测试会 运行 在应用程序的整个生命周期中进行很多次,您可能会发现它是否曾经 return 超出范围,
- 证明您的代码使用了随机源。
假设该文件在您的测试环境中不存在,或者您不知道其内容,或者不希望它在一个测试中正确而在其他测试中不正确,所以我们'需要提供一种方法让测试将 class 指向不同的文件。
我们可以编写以下内容,一次编写一个测试,使其通过并在编写下一个之前进行重构。下面是第三个测试写完后实现前的测试和代码:
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
当我写第二个测试时,我意识到它在没有任何额外代码更改的情况下通过了,所以我考虑删除它。但我推迟了那个决定,写了第三个测试,并发现一旦第三个测试通过,第二个将有价值,因为第二个测试测试该值来自文件但第三个测试没有。
be_in
是测试 return 值是否在已知集合中比 include
更好的方法,因为它将实际值放在 expect
中,其中 RSpec 期待它。
还有其他方法可以控制随机性,因此您可以测试它是否已被使用。例如,如果您使用 sample
,您可以 allow_any_instance_of(Array).to receive(:sample)
和 return 任何您喜欢的。但我喜欢使用 srand
,因为它不需要实现来使用使用随机数生成器的特定方法。
如果文件丢失或为空,您也需要对其进行测试。
这里有一些关于我如何测试这样的东西的想法。我可能不会为像这样的简单函数做所有这些,但对于更复杂的情况,它们仍然是很好的技术:
将把条目读入数组的工作和从数组中随机选择元素的工作分开。
如果你不想在测试中依赖文件系统,你可以编写你的方法来处理一般的 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]
几乎每个随机执行某些操作的 Ruby 方法(如 sample
和 shuffle
)都采用可选的 random:
关键字参数,这允许您提供自己的随机数生成器。如果您的方法遵循相同的约定,则您可以从测试中注入一个假随机数生成器,return 是一个硬编码的假随机数序列:
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'
注意: 在最后一个测试中我实际上不会有两个期望;我只是将其包括在内以展示如何 return 一系列随机值。
此外,在这种情况下,测试假设 sample
如何使用随机数生成器。 sample
可能没问题,但 shuffle
.
可能不行
我正在开发 Rails 应用程序并尝试练习 TDD(使用 RSpec)。我的 lib 目录中有一个文件,其中包含一个字符串列表,以及一个读取该文件并随机 select 列表中的一个字符串的方法。我还没有实现这个方法,因为我正在为如何编写测试该功能而苦苦挣扎。
有很多方法可以从数组中随机 select 一个对象,还有很多很好的回答问题,比如 this one 告诉我如何做到这一点(归结为实现,我可能会使用 Array#sample
)。但我的期望应该是什么?我在想:
expect(array).to include(subject.random_select)
这肯定会断言我的方法返回了一些预期值 — 但是否足以断言该方法每次随机 returns 一个不同的字符串?有哪些替代方法,或者额外的测试可以确保我已经覆盖了这种方法?我真的不能指望 subject.random_select
等于伪造的输入,可以吗?
看来您的方向是正确的。我还会从数组中包含一个伪造的 "sample",并期望数组 不 包含该样本。
See these docs,例如:
expect(array).not_to include("fake sample")
我会首先测试从单行文件中非随机选择单个字符串,然后测试从多行文件中选择字符串,然后我测试选择是随机的。你不能在有限的时间内真正测试随机性,所以你能做的最好的就是
- 测试您的方法 return 的值是否在所需范围内,因为您的测试会 运行 在应用程序的整个生命周期中进行很多次,您可能会发现它是否曾经 return 超出范围,
- 证明您的代码使用了随机源。
假设该文件在您的测试环境中不存在,或者您不知道其内容,或者不希望它在一个测试中正确而在其他测试中不正确,所以我们'需要提供一种方法让测试将 class 指向不同的文件。
我们可以编写以下内容,一次编写一个测试,使其通过并在编写下一个之前进行重构。下面是第三个测试写完后实现前的测试和代码:
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
当我写第二个测试时,我意识到它在没有任何额外代码更改的情况下通过了,所以我考虑删除它。但我推迟了那个决定,写了第三个测试,并发现一旦第三个测试通过,第二个将有价值,因为第二个测试测试该值来自文件但第三个测试没有。
be_in
是测试 return 值是否在已知集合中比 include
更好的方法,因为它将实际值放在 expect
中,其中 RSpec 期待它。
还有其他方法可以控制随机性,因此您可以测试它是否已被使用。例如,如果您使用 sample
,您可以 allow_any_instance_of(Array).to receive(:sample)
和 return 任何您喜欢的。但我喜欢使用 srand
,因为它不需要实现来使用使用随机数生成器的特定方法。
如果文件丢失或为空,您也需要对其进行测试。
这里有一些关于我如何测试这样的东西的想法。我可能不会为像这样的简单函数做所有这些,但对于更复杂的情况,它们仍然是很好的技术:
将把条目读入数组的工作和从数组中随机选择元素的工作分开。
如果你不想在测试中依赖文件系统,你可以编写你的方法来处理一般的
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]
几乎每个随机执行某些操作的 Ruby 方法(如
sample
和shuffle
)都采用可选的random:
关键字参数,这允许您提供自己的随机数生成器。如果您的方法遵循相同的约定,则您可以从测试中注入一个假随机数生成器,return 是一个硬编码的假随机数序列: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'
注意: 在最后一个测试中我实际上不会有两个期望;我只是将其包括在内以展示如何 return 一系列随机值。
此外,在这种情况下,测试假设 sample
如何使用随机数生成器。 sample
可能没问题,但 shuffle
.