测试 rspec example 乱序执行导致测试错误

kepaning · 2014年07月10日 · 最后由 leozwa 回复于 2014年08月10日 · 2807 次阅读

初学 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 执行顺序会有差异。

总结:

  1. rspec 每一个 example 执行前都会去执行一遍 before,example group 里面有多少 example,就会执行多少次 before。
  2. Fabraication 的 sequence 方法只会设置一次 index 初值,之后每运行一次,index 就会加 1。
  3. rspec example 的执行顺序是乱序的,不同的 seed 或许会有不一样的执行顺序,遇到 fail 的时候,记住 seed,使用 --seed 复现。

可以使用 --order default 按序执行

after do
  Video.destroy_all
end

每条 test 应该保证前后文无关联,运行前后数据是干净的

或者也可以

before :all do
end

这样乱序也不怕了

#1 楼 @leozwa 非常感谢,这个问题困扰了很久,另外还有个问题,Rspec 的测试中,如果需要用到--order default, 我感觉就说明不同的 it 测试 block 中存在依赖关系,是否建议每个 it block 尽量互相独立,避免使用--order default 呢?

#4 楼 @dotcomXY 我觉得测试应该是互相独立的 不应该互相依赖或者共享任何东西 项目大了以后如果连测试都互相有依赖 那简直是噩梦

需要 登录 后方可回复, 如果你还没有账号请 注册新账号