RSpec block syntax offers an extremely flexible option for validating the arguments passed in method calls. In this post we look at an example of how to use RSpec block syntax to verify arguments.

Introduction

Like many test frameworks RSpec allows you to stub method implementations and to set expectations in your tests, to verify which methods should be invoked, and with what arguments. This is achieved using either the expect or allow syntax. By setting message expectations in this way our unit tests can assert that some dependency is called in the manner expected, without us having to worry about actually invoking the method on that dependency. For example:

    
RSpec.describe "setting a message expectation with a block" do
  it "returns an instance of Foo" do
    expect(api_client).to receive(:create) { Foo.new }
    result = api_client.create
    expect(result).to be_a Foo
  end
end
    
  

This code shows how we can stub the api_client.create method to return a new instance of Foo. This new instance is created and returned by the block passed to the receive method exposed by RSpec. When the stubbed method is invoked the block we have defined will be yielded along with any arguments passed by the caller. For example:

    
RSpec.describe "setting a message expectation with a block" do
  it "returns an instance of Foo" do
    expect(api_client).to receive(:create) { |name| Foo.new (name: name) }
    result = api_client.create("Bar")
    expect(result.name).to eq "Bar"
  end
end
    
  

We have kept the block pretty simple in these starter examples, but the logic could be arbitrarily complex. This makes the block syntax extremely flexible and useful in a wide variety of cases. Typical examples cited in the docs include:

  • Performing some calculation
  • Simulating transient network failure
  • Verifying arguments
This last example is the one that I have found, by far, the most useful and the rest of this post will discuss this use case by way of an example.

The code we want to test

We will demonstrate how to use the RSpec block syntax to flexibly validate method arguments. To do that we will introduce a UserBuilder class that we wish to test:


class UserBuilder
  attr_reader :api_client,
    :name,
    :shoe_size,
    :south_paw

  # The :api_client must expose two methods:
  # * :create method to create the User
  # * :get_token to return an Authorization token for the API call
  def initialize(api_client: nil,
                 name: "None",
                 shoe_size: 10,
                 south_paw: false)
    raise ArgumentError, ":api_client is required" unless api_client
    unless (3..16).include?(shoe_size)
      raise ArgumentError, ":shoe_size must be between 3-16"
    end
    @api_client = api_client
    @name = name
    @shoe_size = shoe_size
    @south_paw = south_paw
  end

  def build
    api_client.create({
      "Content-Type" => "application/json",
      "Authorization" => "Bearer #{api_client.get_token}"
    }, {
      name: name.downcase,
      shoe_size: convert_uk_to_us(shoe_size),
      south_paw: south_paw
    })
  end

  private

  def convert_uk_to_us(shoe_size)
    shoe_size + 0.5
  end
end


  

The UserBuilder will accept a hash of arguments. The :api_client is required, and provides a means for the UserBuilder to call the API. We don't care how the :api_client works, but it must conform to the interface expected by the UserBuilder, and described in the comments.

As well as the :api_client, the initializer also accepts a few attribtes (:name, :shoe_size and :south_paw). These are given default values if not supplied, with some basic validation applied to the :shoe_size attribute.

NOTE this technique of passing our :api_client into the UserBuilder constructor is an example of dependency-injection. And you will see that this simple trick will make our usage and testing a lot simpler.

The #build method exposed by the UserBuilder class is the main method that we wish to test. We can see that this method really just calls the :api_client with a hash of headers and an appropriatly constructed hash of attributes. The arguments passed to UserBuilder undergo some simple mappings before being passed off to the API, and this really summarizes the job of the UserBuilder class; it takes the raw attributes of the user and it knows how to map these to a form expected to make the API call.

We can see a simple example of how this UserBuilder can be used in the following script:

      
require 'date'
require './user_builder'

dummy_client = Object.new.tap do |client|
  def client.get_token
    "90809080"
  end

  def client.create(*args)
    puts "Making API call with args: #{args}"
  end
end

builder = UserBuilder.new(
  api_client: dummy_client,
  name: "Rocky Balboa",
  shoe_size: 10.5,
  south_paw: true
)

builder.build
      
    
We first create a dummy_client which conforms to the interface required for our API client. We then use this client along with some attributes to create a new UserBuilder instance, upon which we invoke the #build instance method.

So now we know what the UserBuilder is and how to use it, we now want to look at how to test it.

Verifying arguments using RSpec block syntax

To test the #build method, we want to verify that the :api_client is called with appropriate arguments. You can see that the call to :api_client should be sufficiently complicated so as to make you shudder at the thought of testing the message expectations using argument matchers. It is certainly complicated enough to put me off. Instead we are going to look at how we can test these :api_client calls simply by means of the RSpec block syntax.

Without further ado we'll take a look at how we can test the #build method:

      
require "./user_builder"

RSpec.describe UserBuilder do
  before :each do
    @dummy_client = double("api client", {
      create: "Done",
      get_token: "90809080"
    })

    @params = {
      api_client: @dummy_client,
      name: "Apollo Creed",
      shoe_size: 11
    }
  end

  …

  describe "instance method" do
    before :each do
      @builder = UserBuilder.new(@params)
    end

    describe "#build" do
      it "should call the API client" do
        expect(@dummy_client).to receive(:create).and_return("Done")
        @builder.build
      end

      describe "API client call" do
        it "should set headers appropriately" do
          expect(@dummy_client).to receive(:create) do |headers, _body|
            expect(headers["Content-Type"]).to eq "application/json"
            expect(headers["Authorization"]).to eq "Bearer 90809080"
          end.and_return("Done")
          @builder.build
        end

        it "should set the body appropriately" do
          expect(@dummy_client).to receive(:create) do |_headers, body|
            expect(body[:name]).to eq "apollo creed"
            expect(body[:shoe_size]).to eq 11.5
          end.and_return("Done")
          @builder.build
        end
      end
    end
  end
end
      
    
You can appreciate that the technique used hinges on setting up a message expectation on the injected API client, @dummy_client. The block passed to this stubbed method will be invoked when the @dummy_client#create method is called, and the arguments passed in that call (i.e. headers and body in this case) will be accessible in our block:
      
          expect(@dummy_client).to receive(:create) do |headers, body|
            …
          end
      
    
Within the block we can then set our assertions against the method arguments that are passed:
      
          expect(@dummy_client).to receive(:create) do |headers, body|
            expect(body[:name]).to eq "apollo creed"
            …
          end
      
    
The final step in each test is then to actually invoke @builder.build which should trigger our stubbed method and run our assertions.

Summary

RSpec offers block syntax which can be used in a variety of different ways. We looked at using this syntax to get a fine-grained control over verifying arguments passed to method calls. This technique is particularly useful when the arguments to a method call have a complex structure that does not lend itself to testing using regular argument matchers.

References

  1. RSpec Docs
  2. GitHub repo with code from this post

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.