我正在研究Rails应用程序并尝试练习TDD(使用RSpec)。我的lib目录中有一个文件,其中包含一个字符串列表,以及一个读取该文件并从列表中随机选择其中一个字符串的方法。我还没有实现这个方法,因为我正在努力学习如何编写测试功能。
有很多方法可以从数组中随机选择一个对象,并且在这里有很多很好的回答问题,例如this one,告诉我如何做到这一点(当涉及到实现时,我会可能使用Array#sample
)。但我的期望是什么呢?我想的是:
expect(array).to include(subject.random_select)
这肯定会断言我的方法会返回一些预期的值 - 但这足以断言该方法每次都会随机返回一个不同的字符串吗?什么是替代方案,或者可能是确保我已经覆盖这种方法的其他测试?我真的不能指望subject.random_select
等于伪造的输入,我可以吗?
答案 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)
以下是关于我如何测试此类内容的一些想法。我可能不会为这样一个简单的函数做所有这些,但它们仍然是更复杂案例的好技术:
将将条目读入数组的工作分开,以及从数组中选择随机元素的工作。
如果您不想在测试中依赖文件系统,可以编写方法来处理常规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:
关键字参数,它允许您提供自己的随机数生成器。如果你的方法遵循相同的约定,那么你可以从你的测试中注入一个伪随机数生成器,它返回一个硬编码的伪随机数序列:
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
。
答案 2 :(得分:0)
看起来你走在正确的轨道上。我还要包括一个假的"样本"从数组中,并期望数组不包含该样本。
See these docs,例如:
expect(array).not_to include("fake sample")