Creating a RSpec Stub to Mimic Sending SMS Messages
In order to test sending SMS messages we will implement a RSpec Stub that mimics the Twilio API.
Guide Tasks
  • Read Tutorial

When it comes to building applications via the TDD/BDD method, especially when working with APIs, it's important to clearly understand that our tests should only test the features we're building into the application. Some common test driven development mistakes are to test:

  1. The framework (in this case Rails)
  2. The language (Ruby is well tested, it doesn't need your unit tests unless you're planning on contributing to the core language)
  3. The API

I wanted to start this guide by clarifying this because when it comes to building RSpec tests for our microservice it's important that we're not creating tests that attempt to make sure that the Twilio API is working properly. Instead we should simply be creating tests that ensure that we're working with the API properly and for that reason we're going to be implementing what's called a RSpec stub.

What is a stub?

In TDD there are a number of tools that help test suites entail expectations that we can work with, you already have quite a bit of experience working with model factories and, at a high level, stubs are simply data objects that we can use in tests. Stubs make it possible to mimic outside services, such as the Twilio API, so that we can see how our application handles the requests/responses without having to call the API each time we run the test suite.

For a real world example, if you were learning how to become a pilot, you wouldn't start by jumping into the cockpit of a 747, you'd begin in the simulator. In the same way our tests don't need to communicate with the real API, they simply need to work with the same type of data and behavior.

Why should we use stubs?

While it may seem counterintuitive to build tests that only communicate with fake data, however there are a number of reasons why this is the best way to work with APIs:

  • Many APIs limit the number of calls you can make from a single IP address. Therefore if you are working on an application and running the specs 20-30 times a day and your test suite has a dozen API calls you'll end up sending hundreds of API requests.
  • Your test suite speed would grind to a halt. A well constructed set of tests should run quickly, if you're making API calls in your tests you'll be spending all day watching your tests run instead of actually building in features.

Implementing stubs

Now that we know what stubs are and why we should use them, how exactly do we move forward from a practical perspective? Whenever you're mocking an outside service the first step should be to see what the inputs and outputs are for the service. We'll be using the Twilio Ruby Gem, which shows that we have two items that need to occur in order to send text messages:

  1. Setup a Twilio client that configures the connection and instantiates a Twilio Client
  2. Passes in values for the message to be sent

Below would be the basic code needed:

# establish the API credentials
account_sid = 'your account sid'
auth_token = 'your auth token'

# set up a client to talk to the Twilio REST API
@client = Twilio::REST::Client.new account_sid, auth_token

# send the message via the client
@client.messages.create(
  from: '+14159341234',
  to: '+16105557069',
  body: 'Hey there!'
)

It's pretty crazy how easy it is to send SMS messages with Twilio. Now let's create a file for our stub:

touch lib/fake_sms.rb

Now let's add in a module called FakeSms that mimics the behavior of the Twilio API, but also gives some helpful methods such as messages that will let us send a fake SMS message and then verify that it was sent and being able to access the values.

# lib/fake_sms.rb

module FakeSms
  Message = Struct.new(:num, :msg, :app)
  @messages = []

  def self.send_sms(num, msg, app)
    @messages << Message.new(num, msg, app)
  end

  def self.messages
    @messages
  end
end

As is a popular convention with stubs, we have defined a module that encapsulates behavior and data by including a method and the Struct class. This is a nice wrapper that will let us control exactly what the API will do so we can have our tests test the closest thing possible to the actual Twilio API. I also defined a messages method that will act like an ActiveRecord call, so we can do something such as FakeSms.messages.last and we'll get the last sms that was sent along with all of its attributes.

Now we have to update the spec_helper.rb file to let it know that we want to use the FakeSms module instead of the actual API:

# spec/spec_helper.rb
# I removed the comments to make it easier to read
# and that you can see exactly what your spec_helper
# File should look right now

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.before(:each) do
    stub_const("SmsTool", FakeSMS)
  end
end

Here we're telling RSpec to call our FakeSms module instead of the production SmsTool that connects to the API.

Writing the Twilio Stub Specs

With our stub in place let's create our spec. We want the application to send a SMS message each time a notification is created. We already added the call to our SmsTool module in a previous guide so we're pretty close with the implementation. (Side note: we skipped slightly ahead of the red/green/refactor workflow, however I think it was helpful to first learn how to properly setup a module and call it in Rails).

With all of this in mind our test should:

  1. Send an API request
  2. Expect the last SMS to equal the value of the parameters sent in the request

Let's open up the notification request spec and add another test:

# spec/requests/notification_spec.rb

  it 'sends a text message via the Twilio API after a notication is created' do
    headers = {
      "ACCEPT" => "application/json"
    }

    post "/notifications",
    {
      notification: {
        phone: "1234567890",
        body: "New Message",
        source_app: "my_app_name"
      }
    }, headers

    expect(FakeSms.messages.last.num).to eq("1234567890")
  end

This is looking for the last message sent to have the number value of "1234567890". If we run the spec now we'll get an error because our implementation simply has fake data. However it looks like our stub is working properly, so that's a good sign. Let's update the implementation code in the NotificationsController:

# app/controllers/notifications_controller.rb

class NotificationsController < ApplicationController
  include SmsTool

  def create
    @notification = Notification.new(notification_params)

    respond_to do |format|
      if @notification.save
        SmsTool.send_sms(@notification.phone, @notification.body, @notification.source_app)
        format.json { render action: 'show', status: :created, location: @notification}
      else
        format.json { render json: @notification.errors, status: :unprocessable_entity }
      end
    end
  end

  def show
  end

  private

    def notification_params
      params.require(:notification).permit(:phone, :body, :source_app)
    end
end

With this live data it looks like everything should work. If you run rspec you'll see that all of the tests are now passing. Of course this is the tricky part of working with stubs, because even though our workflow may be working properly and our tests are passing, we still need to build in the actual API connection.

In the next guide we'll walk through how we'll implement the SMS sending functionality and complete the integration with the Twilio API.