Chapter 3. Taking Control of State with Doubles and Hooks

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:

  • The role of stubs, mocks, and spies in testing
  • How to use RSpec's double
  • Spying on methods and objects with expect and to_receive
  • Setup and teardown with before and after hooks

Why mock?

A 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:

Why mock?

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset