在 Spec 中测试 Rails 模型,如何创建高效的示例?

Testing Rails model in Spec, how to create efficient examples?

我正在尝试使用 Rspec 和 FactoryBot 在 Rails 中测试我的模型。我正在使用 let!:

let!(:account1) { create(:account, :year) }
let!(:account2) { create(:account, :three_months) }
let!(:account3) { create(:account, :month) }
let!(:account4) { create(:account, :day) }

我正在测试我的示波器是否返回了正确的记录数组。我的假设是还应该有一些 "bad data",来测试范围是否没有返回它不应该返回的东西。例如:

  describe ".seen_last_two_months" do
    subject { Account.seen_last_two_months.to_a }
    it { is_expected.to eq([account3, account4]) }
  end

看来,随着测试量的增加,速度会明显下降。似乎每个调用 let! 的测试都会命中数据库,创建一条新记录。

  1. 我设置测试的方式是否正确,那么我是否也应该测试查询不希望返回的记录?
  2. 有没有办法只调用一次创建记录?
  1. Is the way I'm setting up my tests correct, so should I also test for records that are not expected to be returned by the query?

是的,只是你应该。

  1. Is there a way to invoke the creation of the records only once?

是的,您需要创建这些记录来测试特定范围。因此,它们应该只在特定测试时被调用。您可以将它们移动到与范围测试相关的 describe 块,然后它们将不会被其他测试调用。

# Keep this one without `!`, so it will be called (in other tests), if and when needed
let(:account1) { create(:account, :year) }

describe ".seen_last_two_months" do
  let!(:account1) { create(:account, :year) }
  let!(:account2) { create(:account, :three_months) }
  let!(:account3) { create(:account, :month) }
  let!(:account4) { create(:account, :day) }
  subject { Account.seen_last_two_months.to_a }
  it { is_expected.to eq([account3, account4]) }
end

更新

如果您只想为所有测试创建一次记录,请改用 before(:all)。看起来在 before(:all) 块中调用 let 定义的变量是个坏主意,请改用实例变量:

before(:all) do
  @account1 = create(:account, :year)
  @account2 = create(:account, :three_months)
  @account3 = create(:account, :month)
  @account4 = create(:account, :day)
end

describe ".seen_last_two_months" do
  subject { Account.seen_last_two_months.to_a }
  it { is_expected.to eq([@account3, @account4]) }
end

将测试中出现的所有 account* 替换为 @account*

请记住,对象中任何测试所做的更改也会反映在下一个测试中。

您可以一次创建所有帐户并在其上测试您的所有范围。只需将所有内容移动到 it 块下,如下所示:

context "scopes" do
  it 'selects accounts correctly' do
    let!(:account1) { create(:account, :year) }
    let!(:account2) { create(:account, :three_months) }
    let!(:account3) { create(:account, :month) }
    let!(:account4) { create(:account, :day) }

    expect(Account.seen_last_two_months.to_a).to eq([account3, account4])
    expect(Account.another_scope.to_a).to eq([account1, account2])
    expect(Account.one_more_scope.to_a).to eq([account2])
  end
end

但是这种规格更难维护,您需要在实例中指定许多不同的属性来检查所有范围。我仅将这种方式用于类似的范围,例如按状态划分的范围 ('active'、'inactive'、'archived').

顺便说一句:提供更具描述性的名称总是一个好主意,例如

let!(:year_ago) { create(:account, :year) }