In this chapter, we'll learn how RSpec implements the general testing concepts of stubs, mocks, and spies with double
. We'll start by implementing our own custom mocking method and use it to show off some fun tricks. This will help us understand how mocking works. We'll also appreciate the variety of mocking tools that RSpec offers after we implement one of our own. Then we'll learn how to use RSpec hooks to set up and tear down state related to our tests. Here is what we will cover in this chapter:
double
expect
and to_receive
before
and after
hooksA unit in a real-world software system interfaces with many other units, internal and external. To have focused, isolated tests, we must somehow manage these interactions with other units. There are a few options. One is to simply use the other unit, as in the normal operation of the software in production. This option has the advantage that we don't do anything different in our tests, as compared to real-world usage of the software. However, this approach has two drawbacks. First, it undermines our goal of isolated unit testing by tying the tests for one unit with the exercise of code in another unit. Second, it is often not practical to load and set up the other unit in our tests due to a complex setup, external dependencies, and increased runtime of the test.
A common approach to dealing with the problem of external interfaces in testing is to use a mock in place of the other unit. Given Ruby's dynamic nature, it is quite easy to implement such a feature by using Module#define_method
. Here is a naïve implementation to demonstrate the basic concept:
class Object def self.mock(method_name, return_value) klass = self # store existing method, if there is one, for restoration existing_method = if klass.method_defined?(method_name) klass.instance_method(method_name) else nil end klass.send(:define_method, method_name) do |*args| return_value end # execute the passed block with the mock in effect yield if block_given? ensure # restore klass to previous condition if existing_method klass.send(:define_method, method_name, existing_method) else klass.send(:remove_method, method_name) end end end
Here is how we could use our Object.mock
method to redefine how addition works, by mocking the return value of Fixnum#+
to always return 5000
regardless of which numbers are being passed. Note that in Ruby, 2 + 2
is actually evaluated as 2.+(2)
; that is, the method named +
called on the Fixnum
instance 2
with an argument of 2
:
It's fun to redefine addition like this, but how would we use our mocks in a test? Let's look at an example where we have a ShoppingCart
class that can calculate the total price of the products it contains, each of which is an instance of a Product
class. The real-world functionality depends on a database query to retrieve the list of products, but we want to avoid hitting the database in the unit test for a simple method like ShoppingCart#total_price
, and we can achieve this using our little mock method, like so:
require 'rspec' require_relative 'simple_mock' class ShoppingCart def total_price products.inject(0) do |sum, product| sum += product.price end end # ... end class Product # ... end RSpec.describe ShoppingCart do describe '#total_price' do it "returns the sum of the prices of all products" do num_products = 22 price = 100 cart = ShoppingCart.new some_products = [Product.new] * num_products ShoppingCart.mock(:products, some_products) do Product.mock(:price, price) do expect(cart.total_price).to eq(num_products * price) end end end end end
Our Object.mock
method works very similarly to RSpec's allow_any_instance_of
method, and we can use that to achieve the same result, like this:
context "using RSpec's allow_any_instance_of" do it "returns the sum of the prices of all products" do num_products = 22 price = 100 cart = ShoppingCart.new some_products = [Product.new] * num_products expect_any_instance_of(ShoppingCart).to receive(:products) .and_return(some_products) allow_any_instance_of(Product).to receive(:price) .and_return(price) expect(cart.total_price).to eq(num_products * price) end end
RSpec gives us nicer syntax and also handles the definition and removal of the mock in a much safer and more sophisticated way, accounting for the subtle and tricky behavior of Ruby when it comes to methods and scope.
Note that we use expect_any_instance_of
for the cart
object but allow_any_instance_of
for the product
objects. The cart
object only receives one method call, whereas the product
instance will receive 22 method calls. Had we used expect_any_instance_of
, the test would have failed unless we specified the exact number of times the method should be called. Generally, it's better to use expect-based methods rather than allow-based methods in tests as they can catch unexpected behavior, but sometimes it is not worth the extra clutter, as in this case.
As we saw in this example, with a mock, we can bypass externalities like a database connection so that we can test only the code that interests us. One important thing to note is that this interaction with external systems is a common source of bugs, so if all of our tests used mocks, we would very likely miss some big problems with any part of our code that depended on an external system. Knowing when to use a mock, and when not to, is a subjective and difficult judgment to make. We will return to this question at the end of this chapter.