- 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:
- The framework (in this case Rails)
- The language (Ruby is well tested, it doesn't need your unit tests unless you're planning on contributing to the core language)
- 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:
- Setup a Twilio client that configures the connection and instantiates a Twilio Client
- 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:
- Send an API request
- 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.