Stub on any instance of a class

测试时遇到的一个问题。

背景

新建一个对象,该对象新建后会自动保存,现在需要测试其无法保存的情景,使得obj.save返回为false。

踩坑:

obj这个对象的创建中有一个属性attr设置了必填,同时该attr的值来自另一个对象a, 参考先前的代码,解决的思路是:

通过a.save(validate: false), 让a在attr为空时保存,然后obj创建时,会取a的attr作为自己的attr,这样obj保存时必然是false,因为atrr为空,它无法过检验,但是运行后发现,不管是a.save(validate: false)还是a.save!(validate: false), 再次执行a.save,都会回滚,返回false。虽然它们跳过了validation,但是save时,并没有成功,Google了半天,没有找出好的解决方法。后来,看到同事用stub的方式完美解决了,由衷觉得真是太赞了。

同一个问题,换个人解决,高下立判,只能说,知道和想到,隔了好几个山头啊。

记录下allow_any_instance_of的用法。

allow_any_instance_of

允许一个class的任何实例stub任何一个method的,可设置返回值,也可以不设置,文档中说可以replace allow,但是allow接受的参数是对象,而allow_any_instance_of则是类。

看一些老的版本,比如2.99,会发现allow_any_instance_of的”前身”,any_instance.stub,我以为是前身,但是在该版本的Rspec-mock的message exception部分,已经出现了allow_any_instance_of,有些奇怪,不过用起来倒是都好用的,这里一并下记录下两者的用法。

  • stub 单个method无参

    单个method的stub,看官网的例子:

    ### allow_any_instance_of
    RSpec.describe "allow_any_instance_of" do
      it "returns a specified value on any instance of a class" do
        allow_any_instance_of(Object).to receive(:foo).and_return(:return_value)
        obj = Ojbect.new
        expect(obj.foo).to eq(:return_value)
      end
    end
    

    用any_instance.stub 可以这样写:

    ### any_instance.stub
    RSpec.describe "any_instance.stub" do
      it "returns a specified value on any instance of a class" do
        Object.any_instance.stub(:foo).and_return(:return_value)
        obj = Ojbect.new
        expect(obj.foo).to eq(:return_value)
      end
    end
    
  • stub单个method有参

    带参数的单个method,这样写:

    ### allow_any_instance_of
    RSpec.describe "allow_any_instance_of" do
      context "with arguments" do
        it "returns the stub value when arugments match" do
          allow_any_instance_of(Object).to receive(:foo).with(:param_one, :param_two).and_return(:return_value_one)
          allow_any_instance_of(Object).to receive(:foo).with(:param_another_one, :param_another_two).and_return(:return_value_another_one)
          obj = Ojbect.new
          expect(obj.foo(:param_one, :param_two)).to eq(:return_value_one)
          expect(obj.foo(:param_another_one, :param_another_two)).to eq(:return_value_another_one)
        end
      end
    end
    

    换成any_instance.stub,可以这么来:

    ### any_instance.stub
    RSpec.describe "any_instance.stub" do
      context "with arguments" do
        it "returns the stub value when arugments match" do
          Object.any_instance.stub(:foo).with(:param_one, :param_two).and_return(:return_value_one)
          Object.any_instance.stub(:foo).with(:param_another_one, :param_another_two).and_return(:return_value_another_one)
          obj = Ojbect.new
          expect(obj.foo(:param_one, :param_two)).to eq(:return_value_one)
          expect(obj.foo(:param_another_one, :param_another_two)).to eq(:return_value_another_one)
        end
      end
    end
    
  • stub 多个method无参

    多个method:

    ### allow_any_instance_of
    RSpec.describe "allow_any_instance_of" do
      it "stub multiple methods" do
        allow_any_instance_of(Object).to receive_messages(:foo => "foo", :bar => "bar")
        obj = Ojbect.new
        expect(obj.foo).to eq("foo")
        expect(obj.bar).to eq("bar")
      end
    end
    

    用any_instance.stub:

    ### any_instance.stub
    RSpec.describe "any_instance.stub" do
      it "stub multiple methods" do
        Object.any_instance.stub(:foo => "foo", :bar => "bar")
        obj = Ojbect.new
        expect(obj.foo).to eq("foo")
        expect(obj.bar).to eq("bar")
      end
    end
    

    any_instance.stub还有一些兄弟姐妹,比如any_instance.unstub,any_instance.stub_chain, 可以戳这里了解更多。

warning

官网上提到了慎用allow_any_instance_of,原因除了认为使用该方法is often a design smell,同时也是频频出bug的地方,be careful.

参考

Stub on any instance of a class

any instance