初学 rspec,在写一个 controller test 的时候遇到一个 bug,代码如下:
describe "#search_by_title" do
before do
@video1, @video2 = Fabricate.times(2, :video) do
name Fabricate.sequence(nil, 1) { |i| "my video#{i}"}
end
end
it "returns an array of object which title contains the string" do
expect(Video.search_by_title("video")).to eq([@video1, @video2])
expect(Video.search_by_title("o1")).to eq([@video1])
expect(Video.search_by_title("o2")).to eq([@video2])
end
it "returns empty array if no match title found" do
expect(Video.search_by_title("vedio")).to be_empty
end
end
这段代码的目的是测试 Video model 的 search_by_title 方法。思路是先生成两笔数据,name 分别是 "my video1" 和 "my video2" ,然后测试找到和找不到两种情况。
这段测试代码刚刚写好后 run rspec 是 pass 的。过段时间,我写好其他代码再 run 的时候,fail 了,让我十分困惑。错误信息如下:
expected: [#<Video id: 1, name: "my video3", ...]
got: #<ActiveRecord::Relation []>
我很纳闷,我怎么会产生 name 是 "my video3"的数据呢?
烧死 n 个脑细胞后,我想到,难道 before 代码块运行了两次?加 puts 上去验证,果然,每一个 example 执行都会运行 before。我之前以为 before 在这个 example group 只会运行一次,结果给所有的 example 使用。
before 代码块运行两次又怎么会产生 3 的 index 呢,难道不是每次都从 1 开始生成 sequence 吗?
Fabrication 的 sequence 方法是一个类方法,它的相关源码如下:
class Fabrication::Sequencer
DEFAULT = :_default
def self.sequence(name=DEFAULT, start=nil, &block)
idx = sequences[name] ||= start || Fabrication::Config.sequence_start
...
end
def self.sequences
@sequences ||= {}
end
end
idx 只在第一次被赋初值,以后每一次都加 1。于是 before 代码块运行两次就产生 "my video1", "my video2", "my video3" 和 "my video4" 四个 name。
但是,如果 example 的执行顺序是按照代码书写顺序执行,这个 test 应该能 pass 才对,难道 example 的执行顺序是乱序的?我试图去 rspec 的源码寻找线索,可是 rspec 的源码比 fabrication 复杂得多,我只是找到了一些片段,推断 example 的执行顺序应该是乱序的。加 puts 上代码验证,执行顺序果然是乱序的,不同的 seed 执行顺序会有差异。
总结: