【问题标题】:Ruby: Rspec double confusionRuby:Rspec 双重混淆
【发布时间】:2018-12-15 05:27:48
【问题描述】:

一个失败的测试让我质疑我对什么是测试替身的理解(测试框架是 RSpec)。

据我了解:

模拟是一个假对象,代表被测对象的合作者

假设我有一个Person 类:

class Person 

  def default_number_of_products 
    Product.new 
  end

end

Product 类:

class Product
  def initialize 
    @default_number = 3
  end
end

为了测试对 Person 调用 default_number_of_products 会导致 Product 接收到 new,我编写了一个如下所示的测试:

RSpec.describe Person do 
  let(:person) { Person.new }

  describe '#default_number_of_products' do     
    it 'invokes new on product' do 
      product = double(Product)
      expect(product).to receive(:new)
      person.default_number_of_products
    end
  end
end

失败并返回此错误:

// ♥ rspec spec/lib/person_spec.rb 
F

Failures:

  1) Person#default_number_of_products invokes new on product
     Failure/Error: expect(product).to receive(:new)

       (Double Product).new(*(any args))
           expected: 1 time with any arguments
           received: 0 times with any arguments
     # ./spec/lib/person_spec.rb:23:in `block (3 levels) in <top (required)>'

Finished in 0.01006 seconds (files took 0.1052 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/lib/person_spec.rb:21 # Person#default_number_of_products invokes new on product

另一方面,下面的测试通过了:

RSpec.describe Person do 
  let(:person) { Person.new }
  let(:product) { Product }

  describe '#default_number_of_products' do   
    it 'invokes new on product' do  
      expect(product).to receive(:new)
      person.default_number_of_products
    end
  end
end

问题

  • 我认为可以通过使用双精度对象来消除对协作对象的依赖——这样,无需实现协作类即可编写测试。上述结果表明 我的理解不太对。请问我缺少什么?

【问题讨论】:

    标签: ruby rspec


    【解决方案1】:

    double 只是您为与测试交互而创建的对象,您可以使用class_double 来充当“类”。当您编写product = double(Product) 时,它会创建一些测试变量product,但不会替换现有的Product 类。

    来自:https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/using-a-class-double

    class_double 是作为 instance_double 的补充提供的 区别在于它验证给定类的类方法,而不是 比实例方法。

    此外,它还提供了一个方便的方法 as_stubbed_const 来 用定义的双精度类替换具体类

    这将起作用:

    it 'invokes new on product' do 
      product = class_double(Product).as_stubbed_const
      expect(product).to receive(:new)
      person.default_number_of_products
    end
    

    【讨论】:

      【解决方案2】:

      你已经接近了。以下是如何模拟对 Person.new 的调用:

      首先,让我们让PersonProduct 类更真实一点...

      class Person 
        def default_number_of_products 
          Product.new.default_number
        end
      end
      
      class Product
        attr_reader :default_number
      
        def initialize
          @default_number = 3
        end
      end
      

      现在是模拟Product 类的Person 测试...

      describe Person do
        subject(:person) { described_class.new }
      
        describe '#default_number_of_products' do
          let(:product) { instance_double(Product, default_number: 42) }
      
          before do
            allow(Product).to receive(:new).and_return(product)
          end
      
          it 'returns 42' do
            expect(person.default_number_of_products).to eq(42)
          end
        end
      end
      

      如果执行Product 中的代码,对person.default_number_of_products 的调用将返回3。相反,这个测试监视了Product.new,并返回了一个双精度值来代替真正的Product。因此,当 person.default_number_of_products 中的代码执行时,它会看到 double,它的 default_number 为 42。

      最后,在您上面的问题中,您提到您认为模拟应该允许您创建一个类而不必创建协作者。这是一个真实的陈述。然而,在上面的测试中,instance_double(Product, ...) 实际上是从真实的Product 类创建了一个鸭子类型。所以,它需要被定义。如果您想在创建 Product 类之前对此进行 TDD,您可以传递一个字符串来代替类名,如下所示:

      let(:product) { instance_double('product', default_number: 42) }
      

      HTH

      【讨论】:

      • 我想问一些澄清的问题,但它有点过于复杂,所以我将打开另一个帖子;希望您会看到该帖子并能够提供帮助。
      • 在此线程中保留您的澄清问题通常是个好主意,以便其他有相同问题的人可以在一个地方看到所有答案。
      猜你喜欢
      • 1970-01-01
      • 2016-06-27
      • 2023-03-28
      • 2016-11-08
      • 2016-06-28
      • 2021-08-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多