RSpec模拟对象示例

41

我对模拟对象很陌生,正在尝试学习如何在RSpec中使用它们。请问是否可以提供一个示例(类似于“hello RSpec Mock object world”的示例),或者一个链接(或其他参考资料),介绍如何使用RSpec的模拟对象API?

5个回答

65

这里是我为Rails应用程序的控制器测试编写的一个简单模拟示例:

before(:each) do
  @page = mock_model(Page)
  @page.stub!(:path)
  @page.stub!(:find_by_id)
  @page_type = mock_model(PageType)
  @page_type.stub!(:name)
  @page.stub!(:page_type).and_return(@page_type)
end
在这种情况下,我将Page和PageType模型(对象)进行了模拟,并存根化了我调用的一些方法。
这使我能够运行如下测试:
it "should be successful" do
  Page.should_receive(:find_by_id).and_return(@page)
  get 'show', :id => 1
  response.should be_success
end

我知道这个答案更加关注Rails,但我希望它能在一定程度上帮到你。


编辑

好的,下面是一个"hello world"示例...

给出以下脚本(hello.rb):

class Hello
  def say
    "hello world"
  end
end
我们可以创建以下规范(hello_spec.rb):
require 'rubygems'
require 'spec'

require File.dirname(__FILE__) + '/hello.rb'

describe Hello do
  context "saying hello" do 
    before(:each) do
      @hello = mock(Hello)
      @hello.stub!(:say).and_return("hello world")
    end

    it "#say should return hello world" do
      @hello.should_receive(:say).and_return("hello world")
      answer = @hello.say
      answer.should match("hello world")
    end
  end
end

16
你正在设置一个模拟对象来验证/测试类,这意味着你根本没在使用正在开发的类。毫无意义。模拟旨在帮助你用可信的、尽管更简单/受控/自给自足的对象来包围被测试的类/系统以模拟真实环境。使用模拟来检查正在开发的类的内部工作是反模式。 - Arnaud Meuret
此外,这个答案已经过时了。请参考tokhi在下面的回答来编写一个纯粹的模拟对象,以及我的回答来了解如何在当前的RSpec中进行部分模拟。 - Dave Schweisguth

9

mock已被弃用,基于这个github pull

现在我们可以使用double - 更多信息请参见此处...

 before(:each) do
   @page = double("Page")
   end

  it "page should return hello world" do
    allow(@page).to receive(:say).and_return("hello world")
    answer = @page.say
    expect(answer).to eq("hello world")
  end

5

我没有足够的积分来评论回答,但我想说接受的答案也帮助我尝试找出如何存根随机值。

我需要能够存根对象的实例值,例如随机分配的值:

class ClumsyPlayer < Player do

  def initialize(name, health = 100)
    super(name, health)
    @health_boost = rand(1..10)
  end
end

在我的规格说明中,我遇到了一个问题,即如何对笨拙的玩家的随机生命值进行存根,以测试当他们得到治疗时,他们获得正确的生命值提升。

诀窍是:

@player.stub!(health_boost: 5)

因此,stub!是关键,我之前仅使用stub,但仍然会出现随机的Rspec通过和失败。

所以非常感谢Brian。


1
通常情况下,当您想将一些功能委托给其他对象,但又不想在当前测试中测试实际功能时,您会使用模拟对象,因此将该对象替换为更容易控制的对象。我们称这个对象为“依赖项”...
您正在测试的事物(对象/方法/函数...)可以通过调用方法与此依赖项进行交互以...
  • 查询某些内容。
  • 更改某些内容或产生一些副作用。

调用方法查询内容时

当您使用依赖项来“查询”某些内容时,您不需要使用“模拟API”,因为您可以使用普通对象,并在要测试的对象中测试预期输出...例如:

describe "Books catalog" do
  class FakeDB
    def initialize(books:)
      @books = books
    end

    def fetch_books
      @books
    end
  end

  it "has the stored books" do
    db = FakeDB.new(books: ["Principito"])
    catalog = BooksCatalog.new(db)
    expect(catalog.books).to eq ["Principito"]
  end
end

当调用一个方法来改变某些东西或产生一些副作用时...

当你想要对依赖项进行更改或执行具有副作用的操作(如在数据库中插入新记录、发送电子邮件、进行付款等)时,现在你只需检查是否使用了正确的函数/方法和正确的属性,而不是测试更改或副作用是否已经发生...例如:

describe "Books catalog" do
  class FakeDB
    def self.insert(book)
    end
  end

  def db
    FakeDB
  end

  it "stores new added books" do
    catalog = BooksCatalog.new(db)

    # This is how you can use the Mock API of rspec
    expect(db).to receive(:insert).with("Harry Potter")

    catalog.add_book("Harry Potter")
  end
end

这是一个基本的例子,但你可以凭借这个知识做很多事情 =)
我写了一篇帖子,内容类似并且可能会有用 http://bhserna.com/2018/how-and-when-to-use-mock-objects-with-ruby-and-rspec.html

1
当前(3.x)的RSpec提供了纯模拟对象(如tokhi's answer中所述)和部分模拟(模拟对现有对象的调用)。以下是部分模拟的示例。它使用expectreceive来模拟OrderCreditCardService的调用,以便测试仅在不必实际进行调用的情况下通过。
class Order
  def cancel
     CreditCardService.instance.refund transaction_id
  end
end

describe Order do
  describe '#cancel' do
    it "refunds the money" do
      order = Order.new
      order.transaction_id = "transaction_id"
      expect(CreditCardService.instance).to receive(:refund).with("transaction_id")
      order.cancel
    end
  end
end

在这个例子中,模拟对象是 CreditCardService.instance 的返回值,它可能是一个单例。 with 是可选的;如果没有它,对 refund 的任何调用都将满足期望。可以使用 and_return 给出返回值;在这个例子中它没有被使用,所以该调用返回 nil
这个例子使用了RSpec当前的(expect .to receive)模拟语法,适用于任何对象。接受的答案使用了旧的rspec-rails mock_model方法,它只适用于ActiveModel模型,并已被移出rspec-rails到另一个gem中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接