问题
Within my test I want to stub a canned response for any instance of a class.
It might look like something like:
Book.stubs(:title).any_instance().returns("War and Peace")
Then whenever I call @book.title
it returns "War and Peace".
Is there a way to do this within MiniTest? If yes, can you give me an example code snippet?
Or do I need something like mocha?
MiniTest does support Mocks but Mocks are overkill for what I need.
回答1:
# 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
回答2:
If you're interesting in simple stubbing without a mocking library, then it's easy enough to do this in 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
If you want something more complicated like stubbing all instances of a class, then it is also easy enough to do, you just have to get a little creative:
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
回答3:
I use minitest for all my Gems testing, but do all my stubs with mocha, it might be possible to do all in minitest with Mocks(there is no stubs or anything else, but mocks are pretty powerful), but I find mocha does a great job, if it helps:
require 'mocha'
Books.any_instance.stubs(:title).returns("War and Peace")
回答4:
You can easily stub methods in MiniTest
. The information is available at github.
So, following your example, and using the Minitest::Spec
style, this is how you should stub methods:
# - 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
This a really stupid example but at least gives you a clue on how to do what you want to do. I tried this using MiniTest v2.5.1 which is the bundled version that comes with Ruby 1.9 and it seems like in this version the #stub method was not yet supported, but then I tried with MiniTest v3.0 and it worked like a charm.
Good luck and congratulations on using MiniTest!
Edit: There is also another approach for this, and even though it seems a little bit hackish, it is still a solution to your problem:
klass = Class.new Book do
define_method(:title) { "War and Peace" }
end
klass.new.title.must_equal "War and Peace"
回答5:
You cannot do this with Minitest. However, you can stub any particular instance:
book = Book.new
book.stub(:title, 'War and Peace') do
assert_equal 'War and Peace', book.title
end
回答6:
Just to further explicate @panic's answer, let's assume you have a Book class:
require 'minitest/mock'
class Book; end
First, create a Book instance stub, and make it return your desired title (any number of times):
book_instance_stub = Minitest::Mock.new
def book_instance_stub.title
desired_title = 'War and Peace'
return_value = desired_title
return_value
end
Then, make the Book class instantiate your Book instance stub (only and always, within the following code block):
method_to_redefine = :new
return_value = book_instance_stub
Book.stub method_to_redefine, return_value do
...
Within this code block (only), the Book::new
method is stubbed. Let's try it:
...
some_book = Book.new
another_book = Book.new
puts some_book.title #=> "War and Peace"
end
Or, most tersely:
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
Alternatively, you can install the Minitest extension gem minitest-stub_any_instance
. (Note: when using this approach, the Book#title
method must exist before you stub it.) Now, you can say more simply:
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
If you want to verify that Book#title
is invoked a certain number of times, then do:
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
Thus, for any particular instance, invoking the stubbed method more times than specified raises MockExpectationError: No more expects available
.
Also, for any particular instance, having invoked the stubbed method fewer times than specified raises MockExpectationError: expected title()
, but only if you invoke #verify
on that instance at that point.
回答7:
You can always create a module in your test code, and use include or extend to monkey-patch classes or objects with it. eg (in book_test.rb)
module BookStub
def title
"War and Peace"
end
end
Now you can use it in your tests
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
This allows you to put together more complex behaviours than a simple stub might allow
回答8:
I thought I'd share an example that I built upon the answers here.
I needed to stub a method at the end of a long chain of methods. It all started with a new instance of a PayPal API wrapper. The call I needed to stub was essentially:
paypal_api = PayPal::API.new
response = paypal_api.make_payment
response.entries[0].details.payment.amount
I created a class that returned itself unless the method was amount
:
paypal_api = Class.new.tap do |c|
def c.method_missing(method, *_)
method == :amount ? 1.25 : self
end
end
Then I stubbed it in to PayPal::API
:
PayPal::API.stub :new, paypal_api do
get '/paypal_payment', amount: 1.25
assert_equal 1.25, payments.last.amount
end
You could make this work for more than just one method by making a hash and returning hash.key?(method) ? hash[method] : self
.
来源:https://stackoverflow.com/questions/7211086/how-do-i-stub-things-in-minitest