在我的测试中,我想为某个类的任何实例存根预设响应。
可能会像这样:
Book.stubs(:title).any_instance().returns("War and Peace")
每当我调用@book.title
时,它都会返回“战争与和平”。
在MiniTest中有没有一种方法可以实现这一点?如果有,你能给我一个示例代码片段吗?
还是我需要像mocha这样的东西?
MiniTest确实支持Mocks,但Mocks对我来说有点过头了。
# Create a mock object
book = MiniTest::Mock.new
# Set the mock to expect :title, return "War and Piece"
# (note that unless we call book.verify, minitest will
# not check that :title was called)
book.expect :title, "War and Piece"
# Stub Book.new to return the mock object
# (only within the scope of the block)
Book.stub :new, book do
wp = Book.new # returns the mock object
wp.title # => "War and Piece"
end
我使用 minitest 进行所有的 Gems 测试,但是用 mocha 进行所有的 stubs。也许可以使用 minitest 的 Mocks(没有 stubs 或其他任何东西,但是 mocks 很强大)来完成所有的测试,但我发现 mocha 做得非常好。如果有帮助:
require 'mocha'
Books.any_instance.stubs(:title).returns("War and Peace")
如果您希望进行简单的存根(stubbing)而无需使用模拟库,那么在Ruby中很容易实现:
class Book
def avg_word_count_per_page
arr = word_counts_per_page
sum = arr.inject(0) { |s,n| s += n }
len = arr.size
sum.to_f / len
end
def word_counts_per_page
# ... perhaps this is super time-consuming ...
end
end
describe Book do
describe '#avg_word_count_per_page' do
it "returns the right thing" do
book = Book.new
# a stub is just a redefinition of the method, nothing more
def book.word_counts_per_page; [1, 3, 5, 4, 8]; end
book.avg_word_count_per_page.must_equal 4.2
end
end
end
如果你想要更复杂的东西,比如模拟一个类的所有实例,那么也很容易做到,只需要稍微有些创造力:
class Book
def self.find_all_short_and_unread
repo = BookRepository.new
repo.find_all_short_and_unread
end
end
describe Book do
describe '.find_all_short_unread' do
before do
# exploit Ruby's constant lookup mechanism
# when BookRepository is referenced in Book.find_all_short_and_unread
# then this class will be used instead of the real BookRepository
Book.send(:const_set, BookRepository, fake_book_repository_class)
end
after do
# clean up after ourselves so future tests will not be affected
Book.send(:remove_const, :BookRepository)
end
let(:fake_book_repository_class) do
Class.new(BookRepository)
end
it "returns the right thing" do
# Stub #initialize instead of .new so we have access to the
# BookRepository instance
fake_book_repository_class.send(:define_method, :initialize) do
super
def self.find_all_short_and_unread; [:book1, :book2]; end
end
Book.find_all_short_and_unread.must_equal [:book1, :book2]
end
end
end
MiniTest
中轻松地存根类方法。相关信息可在github上找到。Minitest::Spec
风格,这是您应该存根方法的方式:# - RSpec -
Book.stubs(:title).any_instance.returns("War and Peace")
# - MiniTest - #
Book.stub :title, "War and Peace" do
book = Book.new
book.title.must_equal "War and Peace"
end
klass = Class.new Book do
define_method(:title) { "War and Peace" }
end
klass.new.title.must_equal "War and Peace"
Book.stub :title, "War and Peace"
只能在 title
是 Book
的类方法时起作用。我无法复制 any_instance
的相同行为,出现了错误 NameError: undefined method 'title' for Book'
。 - fguillenstub
操作的是 metaclass = class << self; self; end
。因此似乎只能对单例方法进行桩测试;否则请改用完整的模拟对象。 - Michael De Silvarequire 'minitest/mock'
,但仍然出现NoMethodError: undefined method
stub'`。 - Tom RossiBook.stub_any_instance
来替换一个实例方法,而不是使用 Book.stub
。 - Jesse Mignac为了进一步阐释 @panic的回答,假设你有一个 Book 类:
require 'minitest/mock'
class Book; end
首先,创建一个书籍实例存根,并使其返回您所需的标题(任意次数):
book_instance_stub = Minitest::Mock.new
def book_instance_stub.title
desired_title = 'War and Peace'
return_value = desired_title
return_value
end
然后,在下面的代码块中,让Book类实例化您的Book实例存根(仅且始终如此):
method_to_redefine = :new
return_value = book_instance_stub
Book.stub method_to_redefine, return_value do
...
在这个代码块中(仅限此处),Book::new
方法被桩化。让我们试试:
...
some_book = Book.new
another_book = Book.new
puts some_book.title #=> "War and Peace"
end
require 'minitest/mock'
class Book; end
instance = Minitest::Mock.new
def instance.title() 'War and Peace' end
Book.stub :new, instance do
book = Book.new
another_book = Book.new
puts book.title #=> "War and Peace"
end
如果你愿意,你也可以安装Minitest扩展宝石minitest-stub_any_instance
。(注意:使用此方法时,必须存在Book#title
方法,然后你才能将其stub。)现在,你可以更简单地说:
require 'minitest/stub_any_instance'
class Book; def title() end end
desired_title = 'War and Peace'
Book.stub_any_instance :title, desired_title do
book = Book.new
another_book = Book.new
puts book.title #=> "War and Peace"
end
如果您想验证Book#title
被调用的次数,请执行以下操作:
require 'minitest/mock'
class Book; end
book_instance_stub = Minitest::Mock.new
method = :title
desired_title = 'War and Peace'
return_value = desired_title
number_of_title_invocations = 2
number_of_title_invocations.times do
book_instance_stub.expect method, return_value
end
method_to_redefine = :new
return_value = book_instance_stub
Book.stub method_to_redefine, return_value do
some_book = Book.new
puts some_book.title #=> "War and Peace"
# And again:
puts some_book.title #=> "War and Peace"
end
book_instance_stub.verify
因此,对于任何特定的实例,在调用存根方法的次数超过指定次数时会引发MockExpectationError: No more expects available
异常。
此外,对于任何特定的实例,在调用存根方法的次数少于指定次数时,只有在该实例上调用#verify
时,才会引发MockExpectationError: expected title()
异常。
你不能桩替一个类的所有实例,但你可以像这样桩替给定对象的任何实例方法:
你无法桩替一个类的所有实例,但是你可以像这样桩替给定对象的任何实例方法:
require "minitest/mock"
book = Book.new
book.stub(:title, 'War and Peace') do
assert_equal 'War and Peace', book.title
end
您可以在测试代码中创建一个模块,并使用include或extend将其与类或对象进行Monkey-Patch。例如(在book_test.rb中)
module BookStub
def title
"War and Peace"
end
end
describe 'Book' do
#change title for all books
before do
Book.include BookStub
end
end
#or use it in an individual instance
it 'must be War and Peace' do
b=Book.new
b.extend BookStub
b.title.must_equal 'War and Peace'
end
这允许您组合比简单的存根更复杂的行为
我想分享一个基于这里的答案的示例。
我需要在一长串方法的末尾存根一个方法。它始于一个新的PayPal API包装器实例。我需要存根的调用本质上是:
paypal_api = PayPal::API.new
response = paypal_api.make_payment
response.entries[0].details.payment.amount
我创建了一个类,如果方法不是 amount
,则返回自身:
paypal_api = Class.new.tap do |c|
def c.method_missing(method, *_)
method == :amount ? 1.25 : self
end
end
然后我将它存根到 PayPal::API
中:
PayPal::API.stub :new, paypal_api do
get '/paypal_payment', amount: 1.25
assert_equal 1.25, payments.last.amount
end
您可以通过创建哈希表并返回hash.key?(method) ? hash[method] : self
来使其适用于不止一种方法。