保持干燥,但由于不同原因想要重复

Keep DRY, but want to repeat for different reasons

所以在我为我的 RoR 模型不断发展的 rspecs 中,我最终得到了两个完全相同的测试:

it 'is valid when x is zero' do
    foo = build(:foo, x: 0, y: 10)
    expect(foo.valid?).to be_truthy
end
it 'is valid when y is ten' do
    foo = build(:foo, x: 0, y: 10)
    expect(foo.valid?).to be_truthy
end

这是因为我首先编写了验证 x 的规范,然后添加了 y 的规范。

显然,是时候重构了。我可以删除其中一个规格,因为它们是重复的:保持干燥。

现在,每个规范的内部结构可能完全相同,但 it 描述不同。我不想丢失那里包含的信息。

我的问题是 - 在这种情况下,保持重复规格完整是否可以接受,或者我应该 'merge' 它们并改写 it 描述?也许:

it 'is valid when x is zero and y is ten' do
    foo = build(:foo, x: 0, y: 10)
    expect(foo.valid?).to be_truthy
end

但在我看来,我现在有一个规范正在测试两件事(Foo 模型中的两个验证子句)。那也不好。有一股味道在潜伏。

还有我错过的另一种方法吗?

一般来说,我认为进行小型独立测试比 DRY 更重要。

但是,您的测试逻辑似乎不一致。

如果当 x 为零时 foo 总是 有效,那么您应该能够在第一个规范中删除 y 值。

it 'is valid when x is zero' do
  foo = build(:foo, x: 0)
  expect(foo.valid?).to be_truthy
end

如果 foo 总是 在 y 为 10 时有效,那么您应该能够删除该规范中的 x 值。

it 'is valid when y is ten' do
  foo = build(:foo, y: 10)
  expect(foo.valid?).to be_truthy
end

如果不是这种情况,您可以考虑更具体地测试案例。

例如:

it 'allows x to equal zero' do
  foo = build(:foo, x: 0)
  foo.valid?
  expect(foo.errors).to_not have_key(:x)
end

it 'allows y to be ten' do
  foo = build(:foo, y: 10)
  foo.valid?
  expect(foo.errors).to_not have_key(:y)
end

是的,有些情况下可以保留重复测试。

DRY 代码实践背后的规则并不是一成不变的,它更像是一种启发式的。保持代码 DRY 的主要目的之一主要是出于维护目的。有时人们(包括我自己)觉得您正在努力确保您不会在任何地方重复自己只是为了不重复自己。如果您发现自己为了只写一次东西而增加了不必要的复杂性(我喜欢 Sandy Metz 的一句话“Not so DRY it cafes”),那么您需要问问自己“这真的值得付出努力吗?” “这是否使我的代码更具可读性或可维护性?”。我认为一个很好的试金石是你写的重复代码的实例是否是出于不同的原因而编写的,比如这个实例是为了副作用,这个实例是为了结果。

我不太担心 DRY,而更担心编写实际涵盖您想要的行为的规范。

it 'is valid when x is zero' do
  foo = build(:foo, x: 0)
  expect(foo.valid?).to be_truthy
end

这个例子实际上根本没有涵盖您的验证!如果您在模型中注释掉验证,它仍然会通过。

测试模型验证时的一些技巧:

  • 避免使用工厂。只需使用 .new 和被测属性进行初始化。
  • 测试无效和有效输入。
  • 描述验证的行为 - 而不是可接受的值。
  • 单独测试每个验证 - 您的集成和功能规范通常会涵盖整个验证。

RSpec.describe Foo do
  describe "validations" do
    describe 'x' do
      it "validates that x is a number between 1 and 10" do
        expect(Foo.new(500).valid?.errors[:x]).to include "must be less than or equal to 10".
        expect(Foo.new(10).valid?.errors).to_not have_key :x
      end
    end

    describe 'y' do
      it "validates that y is a number that is less than 15" do
        expect(Foo.new(500).valid?.errors[:y]).to include "must be less than 15".
        expect(Foo.new(10).valid?.errors).to_not have_key :y
      end
    end
  end
end