Test Before Shipping with the Twilio Test Toolkit

Jack Nichols, Creator of TTT

Meet Jack Nichols who is the founder of CareTrio, a startup building better tools to help families care for elderly parents. Jack built a great test toolkit for your Twilio apps, that doesn’t use any of your Twilio minutes – read more about the Twilio Test Toolkit below.

In his spare time, he hikes, climbs, skis, and supports the Seattle Sounders FC. You can find him on Twitter at  @jmongol, and on the web at jmongol.com.

Let’s say you’re building a Twilio application for users to call in and get information about their account. Your users all have an account number and numeric passcode  which they’ll provide to you when they call. Then, they’ll choose an option from a menu. All in all, it’s a pretty common scenario.

This is a straightforward application to implement with Rails and the twilio-ruby gem. You’ll need to write a few actions to return the necessary TwiML and process the digits the user presses along with a few extra bits of code to handle scenarios like a missing account number or the wrong passcode.

But how will you test it? The actions by themselves are easy enough to test in isolation with a few RSpec controller tests, but what about integration tests? Even in this simple scenario, there is enough complexity that an end-to-end test is a really good idea. You could use something like localtunnel or push your changes to your staging environment to test it manually. But that’s a time consuming, error prone, and costs Twilio usage fees.

One approach might be to use something like Capybara to write an integration test for your app. Capybara makes writing complex integration tests easy, but for your Twilio app it can fall a bit short. As Capybara is designed to simulate a web browser interacting with your app, it has functions specific to interacting with page elements, but is limited in what it can do with TwiML or similar data. Capybara is designed around GET requests, and it isn’t so straightforward to use for testing other types of actions.

The twilio-test-toolkit (TTT) gem helps make this easier.  TTT when I was trying to write full integration tests for a complex Twilio app. I wanted a way to write tests from Twilio’s perspective. If Capybara simulates a web browser, I wanted TTT to simulate Twilio calling back one of my app’s methods. I wanted tests written with TTT to be easy to write and understand with minimal fuss and syntax, so I could worry about testing my app, not dealing with Twilio details or parsing XML.

For an app like the example above, TTT with RSpec and Capybara makes it possible to write the following test code:

call = ttt_call(incoming_phone_call_path, from_number, to_number)

We’re on the landing action – make sure we are greeting the user and can collect their account code.

call.should have_say(Hello and welcome to BigCorp)call.within_gather do |gather|gather.should have_say(Please enter your account number)gather.press 12345678

Now we should be on the password action and prompting for one.

call.current_path.should == password_phone_call_pathcall.within_gather do |gather|gather.should have_say(Please enter your password)gather.press 102938

With a successful password, we should be at the menu

call.current_path.should == menu_phone_call_path

Let me give you a sense of what’s going on here.

First, we initiate a “call” to our app with ttt_call. This method simulates what happens when Twilio calls your app back in response to either an incoming or outgoing call. You tell the method which action should be called, as well as the phone number that’s calling and that’s being called. Of course, no actual call is taking place – TTT is simulating the call by POSTing to the action you specified with a set of parameters that you would expect from Twilio.

After your action does its thing, it returns a block of TwiML, which TTT then parses for you and assigns to the call object returned from ttt_call. You can then inspect this object for various properties. In this example, the code is looking for a specific message to appear as a <Say> element. There are other inspection methods too for things like checking for a <Dial> or <Hangup> or similar.

TTT also gives you the ability to interact with your fake call with methods like within_gather. This method allows you to interact with a <Gather> element within your TwiML. You can check for <Say> and other elements, but you can also simulate pressing digits. In the example, you can see the code look for a specific <Say> method to appear within the <Gather>, and then enter the account code we want to test.

When the press method is called, TTT does exactly what Twilio would do – it POSTs to the path specified by the action attribute in the <Gather> element with the digits you specify. Now, your test is interacting with your app as a real user would, sending digits to different actions, “listening” to the <Say> elements in your TwiML, and more. TTT can also inspect and follow redirects embedded in your TwiML too.

As you can see, with just a few short lines of code, we can test full scenarios in a Twilio app, giving us confidence that our apps will work as intended. For the first project I used this on, the app worked the first time I deployed it to production. I only had to fix a few bugs with how Twilio was rendering my <Say> elements (e.g. “one two three” vs. “one hundred twenty three”). Not only was it a huge timesaver, but now whenever the code changes, I can re-run the tests and be confident that nothing is broken. I can also do test-driven development for Twilio apps – just write a few TTT tests, watch them fail, write the code, watch them pass.

TTT is open source (MIT license) and is available as a Ruby gem from https://github.com/JMongol/twilio-test-toolkit or by adding the gem to your gemfile. Documentation is available on the Github page. It should work as-is for most basic needs, but there is plenty to do for any interested contributors. There’s a list of ideas at the bottom of the Github page, and contributions are welcome.

I hope you find TTT useful. Happy coding!