• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
How Test Driven Development started the Robot Apocalypse; Lessons learned using Twilio for Telephony
 

How Test Driven Development started the Robot Apocalypse; Lessons learned using Twilio for Telephony

on

  • 6,610 views

When a client approached us to build a call-center using the Twilio API we didn't realize how far we would push our "test-driven" philosophy. Join us as we explain how easy it was to go from simply ...

When a client approached us to build a call-center using the Twilio API we didn't realize how far we would push our "test-driven" philosophy. Join us as we explain how easy it was to go from simply using a library, to regularly running bots to actually dial our app to ensure its integrity.

Statistics

Views

Total Views
6,610
Views on SlideShare
6,610
Embed Views
0

Actions

Likes
1
Downloads
8
Comments
1

0 Embeds 0

No embeds

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution-NonCommercial-ShareAlike LicenseCC Attribution-NonCommercial-ShareAlike LicenseCC Attribution-NonCommercial-ShareAlike License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

11 of 1 previous next

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
  • Pretty cool. Lot of moving parts. Makes me feel better when my code ends up 'complicated' to know that sometimes, it is complicated.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    How Test Driven Development started the Robot Apocalypse; Lessons learned using Twilio for Telephony How Test Driven Development started the Robot Apocalypse; Lessons learned using Twilio for Telephony Presentation Transcript

    • Sunday, December 16, 12
    • Lessons Learned Using and Testing with Twilio Tips, Tricks, and Best PracticesSunday, December 16, 12
    • How Test Driven Design started the Robot Apocalypse! ... And how its not my fault!Sunday, December 16, 12
    • Customer Call SystemSunday, December 16, 12
    • Wanted New Efficiencies 1. Connecting customers to the same agent; developing a personal relationship. 2. Automatically popping up the customer record on the agents browser. 3. Collect call metrics and tie into other datapoints.Sunday, December 16, 12
    • Current Provider... 1. Offered little to no real-time integration. 2. Was unable or unwilling to customize solution. 3. Was expensive.Sunday, December 16, 12
    • Sunday, December 16, 12
    • Twilio... 1. A REST-ful API to make and manipulate calls and their associated data. 2. Makes real-time callbacks over HTTP to your application about incoming and ongoing calls. 3. Inexpensive: $1 per number, $0.01 per call legSunday, December 16, 12
    • [censored client]Sunday, December 16, 12
    • Sunday, December 16, 12
    • Call Flows 1. A user can click-to-call a target; the users phone is called first and is then connected to the target. 2. A user can enter any number and call it; the users phone is called first and is then connected to the number. 3. If a target calls the mainline, they are immediately connected to a user. 4. Unknown callers to the mainline are placed on a hold queue; any user can handle them.Sunday, December 16, 12
    • DemoSunday, December 16, 12
    • Excellent Documentation https://twilio.com/docsSunday, December 16, 12
    • Productizing Twilio http://kalzumeus.com/2011/12/19/productizing-twilio-applications/Sunday, December 16, 12
    • Treat TwiML as Your View We use Builder to generate XMLSunday, December 16, 12
    • TWiML Builder View xml.instruct! xml.Response do xml.Say Hello. Are you xml.Play https://s3.amazonaws.com/CarbonFive/placeholder.wav xml.Say If not, please hold. xml.Play https://s3.amazonaws.com/CarbonFive/sign_off.wav xml.Enqueue(action: goodbye_twilio_call_path(@call), waitUrl: hold_twilio_call_path(@call)) do xml.text! hold end endSunday, December 16, 12
    • Port-Forward Callbacks To Development Set it up yourself or use: localtunnel http://localtunnel.com forward http://forwardhq.comSunday, December 16, 12
    • Model Calls AND Conversations We made heavy use of state_machine gem https://github.com/pluginaweek/state_machineSunday, December 16, 12
    • Wheres the Robopocalypse?Sunday, December 16, 12
    • TestingSunday, December 16, 12
    • Deprecated SandboxSunday, December 16, 12
    • Test Account Like payment providers, it allows you to make dummy calls, with specific phone numbers resulting in specific responses.Sunday, December 16, 12
    • Record responses with VCR Speeds up test suite. https://github.com/myronmarston/vcrSunday, December 16, 12
    • Conversations make for Messy State MachinesSunday, December 16, 12
    • Sunday, December 16, 12
    • Sunday, December 16, 12
    • On top of all that, we were still figuring it out!Sunday, December 16, 12
    • Changes would blow away functionality!Sunday, December 16, 12
    • Testing became Manual Labor Our poor PM constantly clicking through scenarios.Sunday, December 16, 12
    • Phonio Made use of Twilio JS Client to provide multiple numbers backed by another Twilio account.Sunday, December 16, 12
    • How could we automate this? As part of the build and continuous integration.Sunday, December 16, 12
    • Sunday, December 16, 12
    • I didnt knowSunday, December 16, 12
    • Gaming Bots Scripts that would act as other players to in networked games.Sunday, December 16, 12
    • Capybara RSpec and Cucumber features use it to script a user going through your application.Sunday, December 16, 12
    • Remote Host Capybara.run_server = false Capybara.app_host = http://staging.cyberdyne.com Alternatively require capybara/cucumber require capybara/spec/test_app Capybara.app = TestApp Capybara.app_host = http://staging.cyberdyne.comSunday, December 16, 12
    • Supports Multi-"Users" using_session :ahnold do visit /signin click Terminate, within: #sarah_connor end using_session :robert do visit /sightings/new check have_you_seen_this_boy click Submit endSunday, December 16, 12
    • How to do the same for phones?Sunday, December 16, 12
    • Sunday, December 16, 12
    • Were are NOT testing Twilio.Sunday, December 16, 12
    • We are testing WITH Twilio. An important distinction!Sunday, December 16, 12
    • The Pieces to a Solution were lying around.Sunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ CucumberSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Twilio Account of the BotsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the BotsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the BotsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the Bots CallsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the Bots Bots CallsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the Bots Bots CallsSunday, December 16, 12
    • What does it look like? Twilio Account Capybara The App Browser of the App Rspec/ Cucumber Sinatra Twilio Account of the Bots Bots Calls Twilio ClientSunday, December 16, 12
    • WOPR http://github.com/ZestFinance/wopr http://github.com/carbonfive/woprSunday, December 16, 12
    • A @wopr of a Feature @javascript @wopr Feature: Outbound Call A user can enter phone number so that they can call it Scenario: Simple Session Given a user is logged in And the user enters a phone number And the user clicks the Call button Then the users phone is called And the phone number is called And they are speaking to each otherSunday, December 16, 12
    • Setup in Cucumber require wopr/cucumber require File.join(File.dirname(__FILE__), .., .., staging) Wopr.configure do |config| config.twilio_server_port = 4000 config.twilio_callback_host = http://rudyjahchan.fwd.wf config.twilio_account_sid = TWILIO_ACCOUNT_SID config.twilio_auth_token = TWILIO_AUTH_TOKEN end require File.join(File.dirname(__FILE__), .., .., bots) Wopr::TwilioService.new.update_callbacks([ Wopr::Bot[:ahnold].phone_number, Wopr::Bot[:kyle].phone_number]) Wopr::TwilioCallbackServer.bootSunday, December 16, 12
    • Creat identified Bots Wopr::Bot.create(:ahnold, email: rudy+ahnold@carbonfive.com, password: n07@r3@1p@$$w0rd, phone_number: 5558675309) Wopr::Bot.create(:kyle, phone_number: 5557779311) Wopr::Bot.create(:sarah, phone_number: 9006492568)Sunday, December 16, 12
    • The Goal: Simpler Code Then /^the users phone is called$/ do bot(:ahnold).should be_on_a_call end Then /^the users phone is not called$/ do bot(:ahnold).should_not be_on_a_call end Then /^the phone number is called$/ do bot(:kyle).should be_on_a_call end Then /^they are speaking to each other$/ do bot(:kyle).should be_on_a_call_with(bot(:ahnold)) end Given /^an unknown caller dials the main line$/ do bot(:kyle).make_a_call_to(CYBERDYNE_STAGING_PHONE_NUMBER) endSunday, December 16, 12
    • How does it work?Sunday, December 16, 12
    • Stole a LOT from Capybara Particularly threading code not to block running specs.Sunday, December 16, 12
    • Sinatra App module Wopr class TwilioCallbackServer < Sinatra::Base VERIFICATION_PHRASE = SHALL WE PLAY A GAME? set :views, File.join(File.dirname(__FILE__), templates) get /__identify__ do [200, {}, VERIFICATION_PHRASE] end post /calls do # ... end # ... end endSunday, December 16, 12
    • Mount on Rack def run_server(port) require rack/handler/thin Thin::Logging.silent = true Rack::Handler::Thin.run(self, Port: port) rescue LoadError require rack/handler/webrick Rack::Handler::WEBrick.run(self, Port: port, AccessLog: [], Logger: WEBrick::Log::new(nil, 0)) endSunday, December 16, 12
    • Launch in a thread def boot(port=Wopr.twilio_server_port) @port = port unless responsive? @server_thread = Thread.new { run_server(@port) } end Timeout.timeout(60) { @server_thread.join(0.1) until responsive? } end def responsive? return false if @server_thread && @server_thread.join(0) res = Net::HTTP.start(127.0.0.1, @port) do |http| http.get(/__identify__) end if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection) return res.body == VERIFICATION_PHRASE end rescue Errno::ECONNREFUSED, Errno::EBADF return false endSunday, December 16, 12
    • Server manages Calls post /calls do if(call = Call.find_by_sid(params[:CallSid])) call.update params else Call.create(params) end builder :default endSunday, December 16, 12
    • <Say /> Keep-alive xml.instruct! xml.Response do xml.Say(loop: 0) do xml.text! <<GIBBERISH Yorn desh born, der ritt de gitt der gue, Orn desh, dee born desh, de umn bork! bork! bork! GIBBERISH end endSunday, December 16, 12
    • Bots can examine Calls module Wopr class Bot # ... def current_call Call.find_all_by_number(phone_number).select{|call| call.status != completed}.last end def on_a_call? wait_until do current_call end end # ... end endSunday, December 16, 12
    • Handle Asynchronicity def eventually(seconds=Wopr.default_wait_time) start_time = Time.now begin yield rescue => e raise e if (Time.now - start_time) >= seconds sleep 1 retry end end def wait_until(seconds=Wopr.default_wait_time) eventually(seconds) do result = yield return result if result raise ConditionNotMetError end rescue ConditionNotMetError return false endSunday, December 16, 12
    • Bot makes calls w/ Call module Wopr class Bot # ... def make_a_call_to(phone_number) Call.make(from: self.phone_number, to: phone_number) end # ... end end module Wopr class Call class << self def make(options) TwilioService.new.make(options) end # ...Sunday, December 16, 12
    • TwilioClientService require twilio-ruby module Wopr class TwilioService def initialize @twilio_client = Twilio::REST::Client.new( Wopr.twilio_account_sid, Wopr.twilio_auth_token ) end def make(options) calls.create(options.merge( url: "#{Wopr.twilio_callback_host}/calls" )) end def hangup(sid) call(sid).hangup end # ...Sunday, December 16, 12
    • Do we KNOW theyre TALKING to each other? Its possible the system made two phone calls, but they’re not with each other.Sunday, December 16, 12
    • A Solution: Play and Detect Dial Tones! One bot starts to <Gather> digits, the other <Plays> them, and we confirm they receive it.Sunday, December 16, 12
    • Call Gather & Play module Wopr class Call # ... def play(digits) TwilioService.new.play sid, digits end def gather TwilioService.new.gather sid end # ... end endSunday, December 16, 12
    • Redirect Calls to TWiML module Wopr class TwilioService # ... def play(sid, digits) call(sid).redirect_to( "#{Wopr.twilio_callback_host}/calls/#{sid}/play?digits=#{digits}" ) end def gather(sid) call(sid).redirect_to( "#{Wopr.twilio_callback_host}/calls/#{sid}/gather" ) end # ... end endSunday, December 16, 12
    • Prepare to <Gather /> module Wopr class TwilioCallbackServer < Sinatra::Base # ... post /calls/:sid/gather do builder :gather, locals: { sid: params[:sid] } end # ... end end gather.builder xml.instruct! xml.Response do xml.Gather( timeout: "60", action: "#{Wopr.twilio_callback_host}/calls/#{sid}/gathered", numDigits: "4") endSunday, December 16, 12
    • <Play digits="..." /> module Wopr class TwilioCallbackServer < Sinatra::Base # ... post /calls/:sid/play do builder :play, locals: { sid: params[:sid], digits: params[:digits] } end # ... end end play.builder xml.instruct! xml.Response do xml.Play(digits: digits) xml.Pause(length: 10) endSunday, December 16, 12
    • Gathered Digits Posted module Wopr class TwilioCallbackServer < Sinatra::Base # ... post /calls/:sid/gathered do if(call = Call.find_by_sid(params[:sid])) call.gathered params[:Digits] end builder :default end # ... end endSunday, December 16, 12
    • Again Asynchronous! module Wopr class Bot # ... def on_a_call_with?(another_bot) current_call.gather sleep 1 another_bot.current_call.play 6661 wait_until do current_call.gathered_digits.last == 6661 end end # ... end endSunday, December 16, 12
    • Dial-Tone not a Perfect Solution. What if tones are used to trigger actions? And how do we confirm audio FILE playback?Sunday, December 16, 12
    • <Record> and SOX Retrieve the recording, digest with SOX audio library.Sunday, December 16, 12
    • More Capybara Tie-ins? Given /^a user is logged in$/ do bot(:ahnold).log_in end Given /^the user enters a phone number$/ do bot(:ahnold).within(div#call) do fill_in number, with: bot(:kyle).phone_number end endSunday, December 16, 12
    • How far can we take it?Sunday, December 16, 12
    • [pic]Sunday, December 16, 12
    • Sample Use of wopr http://github.com/carbonfive/cyberdyne-systemsSunday, December 16, 12
    • Questions? rudy@carbonfive.com @rudy on TwitterSunday, December 16, 12
    • Hasta la vista, baby!Sunday, December 16, 12