Skip to content

Commit

Permalink
Initial commit from fork
Browse files Browse the repository at this point in the history
  • Loading branch information
JMongol committed Sep 17, 2012
1 parent 66698bf commit 53601e9
Show file tree
Hide file tree
Showing 56 changed files with 1,750 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .gitignore
@@ -0,0 +1,11 @@
.bundle/
log/*.log
pkg/
test/dummy/db/*.sqlite3
test/dummy/log/*.log
test/dummy/tmp/
test/dummy/.sass-cache

.DS_Store

*.sqlite3
17 changes: 17 additions & 0 deletions Gemfile
@@ -0,0 +1,17 @@
source "http://rubygems.org"

# Declare your gem's dependencies in twilio-test-toolkit.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
# development dependencies will be added by default to the :development group.
gemspec

# jquery-rails is used by the dummy application
gem "jquery-rails"

# Declare any dependencies that are still in development here instead of in
# your gemspec. These might include edge Rails or gems from your path or
# Git. Remember to move these dependencies to your gemspec before releasing
# your gem to rubygems.org.

# To use debugger
# gem 'debugger'
141 changes: 141 additions & 0 deletions Gemfile.lock
@@ -0,0 +1,141 @@
PATH
remote: .
specs:
twilio-test-toolkit (1.0.0)
capybara (~> 1.0)

GEM
remote: http://rubygems.org/
specs:
actionmailer (3.2.8)
actionpack (= 3.2.8)
mail (~> 2.4.4)
actionpack (3.2.8)
activemodel (= 3.2.8)
activesupport (= 3.2.8)
builder (~> 3.0.0)
erubis (~> 2.7.0)
journey (~> 1.0.4)
rack (~> 1.4.0)
rack-cache (~> 1.2)
rack-test (~> 0.6.1)
sprockets (~> 2.1.3)
activemodel (3.2.8)
activesupport (= 3.2.8)
builder (~> 3.0.0)
activerecord (3.2.8)
activemodel (= 3.2.8)
activesupport (= 3.2.8)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activeresource (3.2.8)
activemodel (= 3.2.8)
activesupport (= 3.2.8)
activesupport (3.2.8)
i18n (~> 0.6)
multi_json (~> 1.0)
addressable (2.3.2)
arel (3.0.2)
builder (3.0.3)
capybara (1.1.2)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
selenium-webdriver (~> 2.0)
xpath (~> 0.1.4)
childprocess (0.3.5)
ffi (~> 1.0, >= 1.0.6)
diff-lcs (1.1.3)
erubis (2.7.0)
ffi (1.1.5)
hike (1.2.1)
i18n (0.6.1)
journey (1.0.4)
jquery-rails (2.1.2)
railties (>= 3.1.0, < 5.0)
thor (~> 0.14)
json (1.7.5)
libwebsocket (0.1.5)
addressable
mail (2.4.4)
i18n (>= 0.4.0)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.19)
multi_json (1.3.6)
nokogiri (1.5.5)
polyglot (0.3.3)
rack (1.4.1)
rack-cache (1.2)
rack (>= 0.4)
rack-ssl (1.3.2)
rack
rack-test (0.6.1)
rack (>= 1.0)
rails (3.2.8)
actionmailer (= 3.2.8)
actionpack (= 3.2.8)
activerecord (= 3.2.8)
activeresource (= 3.2.8)
activesupport (= 3.2.8)
bundler (~> 1.0)
railties (= 3.2.8)
railties (3.2.8)
actionpack (= 3.2.8)
activesupport (= 3.2.8)
rack-ssl (~> 1.3.2)
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
rake (0.9.2.2)
rdoc (3.12)
json (~> 1.4)
rspec (2.11.0)
rspec-core (~> 2.11.0)
rspec-expectations (~> 2.11.0)
rspec-mocks (~> 2.11.0)
rspec-core (2.11.1)
rspec-expectations (2.11.2)
diff-lcs (~> 1.1.3)
rspec-mocks (2.11.2)
rspec-rails (2.11.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec (~> 2.11.0)
rubyzip (0.9.9)
selenium-webdriver (2.25.0)
childprocess (>= 0.2.5)
libwebsocket (~> 0.1.3)
multi_json (~> 1.0)
rubyzip
sprockets (2.1.3)
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sqlite3 (1.3.6)
sqlite3-ruby (1.3.3)
sqlite3 (>= 1.3.3)
thor (0.16.0)
tilt (1.3.3)
treetop (1.4.10)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.33)
uuidtools (2.1.3)
xpath (0.1.4)
nokogiri (~> 1.3)

PLATFORMS
ruby

DEPENDENCIES
jquery-rails
rails (~> 3.2.8)
rspec
rspec-rails
sqlite3
sqlite3-ruby
twilio-test-toolkit!
uuidtools
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright 2012 YOURNAME

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
190 changes: 189 additions & 1 deletion README.md
@@ -1,2 +1,190 @@
twilio-test-toolkit
===================
===================

Twilio Test Toolkit (TTT) makes writing RSpec integration tests for Twilio phone callbacks easy to write and understand.

When you initiate a [phone call with Twilio](http://www.twilio.com/docs/api/rest/making-calls), you must POST either a URL or an ApplicationSid that is configured with a URL. Twilio then POSTs to your URL, and you're expected to return a 200 OK and a valid [TwiML](http://www.twilio.com/docs/api/twiml) response. That response can be any valid TwiML - speak something, gather keystrokes, redirect, etc.

Although it's pretty easy to test individual controller actions with the existing RSpec gem, testing more complex scenarios that use many controller actions (or controllers) is syntax-heavy and usually repetitive. TTT exists to make these larger scale integration tests easy and fun to write. TTT emulates the Twilio end of the Twilo phone callbacks, and allows to to emulate a user listening for a specific <Say> element, pressing keys on their phone, etc. With TTT, you can test your whole phone system built on Twilio.

For instance, let's say you have a controller that handles inbound Twilio calls and asks the user to enter an account number, then a PIN code, and then finally makes a menu selection. With TTT, you can just do this:

@call = ttt_call(some_controller_action_path, from_number, to_number)

@call.should have_say("Welcome to AwesomeCo.")
@call.within_gather do |gather|
gather.should have_say("Please enter your account number, followed by the pound sign.")
gather.press "12345678"
end

@call.current_path.should == some_other_controller_action_path
@call.within_gather do |gather|
gather.should have_say("Please enter your 4 digit PIN code.")
gather.press "9876"
end

@call.current_path.should == yet_another_controller_action_path
@call.should have_say "Please choose from one of the following menu options"

TTT was originally built to handle testing a complex, large-scale phone tree system. Without TTT, it was necessary to do lots of deployments to staging to test how the controller actions worked together, and there wasn't a good automated way to verify any new changes. When new functionality was introduced, TTT tests were written for the full project, and everything that worked in TTT worked great the first time it was pushed to staging. It's a great timesaver.

What's supported
================

TTT supports most of the more common Twilio scenarios:

* Checking for <Say> elements and their content
* Taking action on <Gather> elements and querying their contents
* Following and querying redirects
* Dial and Hangup

TTT doesn't yet support (but you should contribute them!)

* <Play>
* <Queue>
* Any of the other conference calling features

What's required
================

TTT depends on [Capybara](https://github.com/jnicklas/capybara). It uses Capybara's session object to POST requests to your controllers, so get Capybara set up before you use TTT. Because of this, your TTT tests should normally go in spec/requests, unless you've relocated these tests elsewhere.

TTT expects your controller actions to behave like well-behaved Twilio callbacks. That is, you need to respond to XML-formatted requests, and need to respond with a 200 OK. TTT also requires that your controller actions be wired for POST, not GET (Twilio does support GET, but TTT lacks this support). Twilio will not follow 301 or 302 redirects properly, and neither will TTT (see below for more details).

TTT has only been tested with RSpec on Rails. It might work on other test frameworks or other Rack-based frameworks. Feel free to submit pull requests to improve compatibility with these.

If it works with Twilio, it should work with TTT. If not, open an issue/pull request.

How to use
================

This section describes how to use TTT and its basic functionality.

ttt_call
-------------

The ttt_call method is the main entry point for working with TTT. You call this method to initiate a "Twilio phone call" to your controller actions. TTT simulates Twilio's behavior by POSTing to your action with the expected Twilio parameters (From, To, CallSid, etc).

ttt_call has three required parameters, and two optional ones:

@call = ttt_call(action_path, from_number, to_number, call_sid = nil, is_machine = false)

* **action_path**. Where to POST your request. It should be obvious by now, but whatever action you specify here should be a POST action.
* **from_number**. What to fill params[:From] with. If you don't care about this value, you can pass a blank one, as this is only used to pass along to your actions.
* **to_number**. What to fill params[:To] with.
* **call_sid**. Specify an optional fixed value to be passed as params[:CallSid]. This is useful if you are expecting a specific SID. For instance, a common pattern is to initiate a call, store the SID in your database, and look up the call when you get the callback. If you don't pass a SID, TTT will generate one for you that's just a UUID.
* **is_machine**. Controls params[:AnsweredBy]. See Twilio's documentation for more information on how Twilio uses this.

ttt_call returns a TwilioTestToolkit::CallInProgress object, which is a descendent of TwilioTestToolkit::CallScope. You'll want to save this object as it's how you interact with TTT.

It's worth noting that TTT won't pass any of the parameters you need to [validate that a request comes from Twilio](http://www.twilio.com/docs/security). Most people seem to only do this check in production, so if that applies to you, this won't be an issue. If you do this check in dev and test, you might want to consider submitting a pull request with a fix.

Getting call information
--------------

You can use the CallInProgress object returned from *ttt_call* to inspect some basic properties about the call:

@call.sid # Returns the SID of the call
@call.initial_path # Returns the path that you used to start the call (the first parameter to ttt_call)
@call.from_number # Returns the from number
@call.to_number # Returns the to number
@call.is_machine # Returns the answering machine state (passed to ttt_call)

Call scopes
--------------

The CallInProgress object returned from *ttt_call* is a descendent of CallScope. A CallScope represents a scope within a call. For instance, the root TwiML <Response> element is a scope. A <Gather> within that is a scope.

For instance, let's say you have TwiML like this:

<Response>
<Say>Foo</Say>
<Gather action="baz">
<Say>Bar</Say>
</Gather>
</Response>

The scope referred to by the CallInProgress object is the <Response> object. Only items that directly descend from this scope are seen by TTT. That is, the say for "Foo" is in the scope, but the say for "Bar" is not. The <Gather> is its own scope, and it contains the say for "Bar". TTT intentionally restricts you to accessing only what's in your scope because it helps you enforce a more rigid structure in your call, and allows you to handle multiple <Gathers> or similar in a given TwiML markup.

CallScope has some properties that are also useful:

@call.current_path # Returns the current path that the call is on.
@call.response_xml # Returns the raw XML response that your action returned to TTT
@call.root_call # Returns the original CallInProgress returned from ttt_call.

Inspecting the contents of the call
--------------

A common thing you'll want to do is inspect the various <Say> elements, and check for control elements like <Dial> and <Hangup>.

@call.has_say?("Foo") # Returns true if there's a <Say> in the current scope that contains the text. Partial matches are OK, but the call is case sensitive.
@call.has_dial?("911") # Returns true if there's a <Dial> in the current scope for the number. Partial matches are OK.
@call.has_hangup? # Returns true if there's a <Hangup> in the current scope.

These methods are available on any CallScope.

Gathers
--------------

Gathers are used to collect digits from the caller (e.g. "press 1 to speak with a representative, press 2 to cancel"). Twilio handles Gathers by speaking the contents of the Gather and waiting for a digit to be pressed. If nothing is pressed, it continues with the script. If something is pressed, it aborts processing the current script and POSTs the digits (via params[:Digits]) to the path specified by the action attribute. TTT handles Gathers in a similar way.

You can only interact with a gather by calling within_gather:

@call.within_gather do |gather|
gather.should have_say("Enter your account number")
gather.press "12345"
end

*within_gather* creates a new CallScope and passes it to the yielded parameter (gather in the example above). *within_gather* will fail if there is no <Gather> element in the current scope.

You can verify the existence of a <Gather> in the current scope with:

@call.has_gather? # Returns true if the current scope has a gather.

Within a gather CallScope, you can use the following methods:

@call.gather? # Returns true if the current scope **is** a gather. Compare with the has_gather? method.
@call.gather_action # Returns the value of the action attribute for the gather.
@call.press("1") # Simulates pressing the specified digits.

You can also use other CallScope methods (e.g. *has_say?* and similar.)

The press method has a few caveats worth knowing. It's only callable once per gather - when you call it, TTT will immediately POST to the gather's action. There is no way to simulate pressing buttons slowly, and you don't really need to do this anyways - Twilio doesn't care. *press* simply fills out the value of params[:Digits] and POSTs that to your method.

Although you can technically pass whatever you want to *press*, in practice Twilio only sends digits and #. Still it's probably a good idea to test garbage data in this parameter with your actions, so TTT doesn't get in your way if you want to call press with "UNICORNSANDPONIES" as a parameter.

TTT doesn't attempt to validate your TwiML, so it's worth knowing that <Gather> only allows <Say>, <Pause>, and <Play> as child elements. Nested <Gather>s are not supported.

Redirects
--------------

The <Redirect> element is used to tell Twilio to POST to a different page. It differs from a standard 301 or 302 redirect (created by a *redirect_to*) in that the 301/302 redirects don't support a POST, and if you try a *redirect_to* within a Twilio action, Twilio will fail and your caller will get the dreaded "I'm sorry, an application error has occurred" message. Because Twilio doesn't support 301/302 redirects, TTT doesn't either, and if you use one, TTT will complain.

There are several methods you can use within a CallScope related to redirects:

@call.has_redirect? # Returns true if there's a <Redirect> element in the current scope.
@call.has_redirect_to?(path) # Returns true if there's a <Redirect> element to the specified path in the current scope.
@call.follow_redirect # Follows the <Redirect> in the current scope and returns a new CallScope object. The original scope is not modified.
@call.follow_redirect! # Follows the <Redirect> in the current scope and updates the scope to the new path.

Although it's allowed by TwiML (as of this writing), multiple <Redirect>s in a scope aren't effectively allowed, as only the first one will ever be used. Thus, TTT only looks at the first <Redirect> it finds in the given scope.

Contributing
================

TTT is pretty basic, but it should work for most people's needs. You might consider helping to improve it. Some things that could be done:

* Support more Twilio functionality
* Support GET actions. Twilio technically supports this, although it doesn't seem to be used very often.
* Build more stringent checks into says and plays - e.g. verify that there's enough of a pause between sentences.
* Build checks to make sure that numbers, dates, and other special text is spoken properly (e.g. "one two three" instead of "one hundred twenty three").
* Build checks to validate the correctness of your TwiML.
* Refactor wonky code (there's probably some in there somewhere)
* Write more tests. There are basic tests, but more involved ones might be nice

Contributions are welcome and encouraged. The usual deal applies - fork a branch, add tests, add your changes, submit a pull request.

Credits
================

TTT was put together by Jack Nichols [@jmongol](http://twitter.com/jmongol). MIT license.

0 comments on commit 53601e9

Please sign in to comment.