Sipper is a good framework for doing functional automation of any sip call flow. Cucumber is a great tool for writing your functional test cases in plain text and execute them. Cucumber is mostly used for doing web automation with other tools like Watir. But now a days, VOIP meets Web now and then, And you are left with the challenge of integrating your web automation with your sip automation. For example, you want to automate “When a call comes to my sip phone, I want to see a javascript popup in my screen”
Sipper comes with its testing/assertion/validation framework. First things first, we need to drop them. We are only interested into the Sip Stack and Call abstraction framework provided by sipper. We will do the assertion from our Cucumber framework.
First of all, you need to be able to run sipper in standalone mode rather than using the ‘rake’ provided by sipper framework. You need to bootstrap sipper framework from your ruby code. You can do that with the following code (usually you would want to put it in your support/env.rb configuration file for cucumber) :
require 'sipper' require 'sipper_configurator'</code> cf = File.join("C:/dev/projects/myproject/qa/sipper_tests/cucumber_tests/", "config", "sipper.cfg") SipperConfigurator.load_yaml_file(cf) @sipper = SIP::Sipper.new() @sipper.start
Now that you have started sipper, you can register any controller in it from your cucumber steps. Lets say we have a step like this:
Then I start customer's phone
You would want to start the controller that ‘REGISTER’ for the customer’s phone and wait for any incoming call.
Then /^I start customer's phone$/ do puts "Going to start Controller: CallReceiverCustomerController" @customer_controller = @sipper.start_named_controller("CallReceiverCustomerController") sleep 3 end
I have extended the standard sipper controller so that I can call it and control it from any ruby application. Basically this will act as a SIP phone and provide me the nice API’s
require 'sip_test_driver_controller' # NOTE: Extending Core Sipper controller to provide programmable API to simple RUBY client. The ruby client # can directly call this controller to control the call flows. class CommonCallController < SIP::SipTestDriverController # change the directive below to true to enable transaction usage. # If you do that then make sure that your controller is also # transaction aware. i.e does not try send ACK to non-2xx responses, # does not send 100 Trying response etc. transaction_usage :use_transactions=>false # change the directive below to true to start after loading. start_on_load false def initialize logd('CallReceiverController: Controller created') clear_states() end def clear_states() @registered = false @current_session = nil @call_ended = false @got_invite=false end def specified_transport log " specified_transport #{get_local_sipper_ip()} : #{get_local_sipper_port()}" [get_local_sipper_ip(),get_local_sipper_port()] end def start if required_registration register() end return self end def register() log "doing registration with " + get_line_port session = create_udp_session(SipperConfigurator[:DefaultRIP], SipperConfigurator[:DefaultRP]) r = session.create_initial_request("REGISTER", "sip:" + SipperConfigurator[:BroadworksDomain] , :from=> "sip:"+ get_line_port, :to=>"sip:"+ get_line_port, :contact=> "sip:" + get_line_port_without_domain + "@"+ get_local_sipper_ip() +":"+get_local_sipper_port().to_s(), :expires=>"300", :cseq=>"1 REGISTER", :p_session_record=>"msg-info") r.contact.q="0.9" session = create_udp_session(SipperConfigurator[:DefaultRIP], SipperConfigurator[:DefaultRP]) session.send(r) @current_session = session log "---------session: #{session}" end def required_registration true end def on_success_res_for_register(s) @current_session = s log "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ on_success_res_for_register" @registered = true s.invalidate(true); end def on_success_res(s) @current_session = s log '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ on_success_res ->' @registered = true s.invalidate(true); end def on_failure_res(session) sleep 10 log "on_failure_res error code: #{session.iresponse.code}" puts "\n -----------------------------**************** #{caller}" if session.iresponse.code == 401 r = session.create_request_with_response_to_challenge(session.iresponse.www_authenticate, false,get_auth_id, get_auth_password) session.send r session[:auth] = r.authorization end end def is_ringing() return @got_invite end #for the first invite it will send 180, for all other re-invite, it will send 200 def on_invite(session) if !session['invite'] log "got invite------------" @current_session = session log 'got call from Queue ' session.respond_with(100) session.respond_with(180) session['invite']=1 @got_invite=true else log "got re invite------------" log 'CallReceiverController: got re-invite' if @accept_call session.respond_with(200) end end end def make_call(uri) log "make_call : #{uri}" session = create_udp_session('10.0.0.27', 5060) session.request_with('INVITE', uri) #@current_session = session end def accept_call() log "accept_call #{@current_session}" @accept_call=true @current_session.respond_with(200) end def end_call() log "ending call #{@current_session}" if @current_session log 'sending bye' @current_session.rp = SipperConfigurator[:DefaultRP] @current_session.request_with('BYE') end end def on_bye(session) log "*******************got bye" @call_ended = true session.respond_with(200) session.invalidate(true) end def is_call_ended() return @call_ended end def last_session_state @current_session.last_state end def get_local_sipper_port SipperConfigurator[:LocalSipperPort][4] end protected :get_local_sipper_port def get_local_sipper_ip SipperConfigurator[:LocalSipperIP] end protected :get_local_sipper_ip def get_line_port SipperConfigurator[:AgentLinePort] end protected :get_line_port def controller_name self.class.name end def get_line_port_without_domain SipperConfigurator[:AgentLinePortWithoutDomain].to_s end protected :get_line_port_without_domain def get_auth_password SipperConfigurator[:CustomerAuthenticationPassword] end protected :get_auth_password def get_auth_id SipperConfigurator[:CustomerAuthenticationUserId] end protected :get_auth_id def log(argument) puts controller_name + " : " + argument end end
Our CallReceiverCustomerController is a simple subclass of the CommonCallController to provide the SIP Registration Details so that it can register to any sip provider (in our case Broadworks).
require 'common_call_controller' class CallReceiverCustomerController < CommonCallController def initialize puts "initialize" super() log 'CallReceiverControllerAgent: Controller created' end def get_local_sipper_port SipperConfigurator[:LocalSipperPort][6] end protected :get_local_sipper_port def get_local_sipper_ip SipperConfigurator[:LocalSipperIP] end protected :get_local_sipper_ip def get_line_port SipperConfigurator[:AgentLinePort] end protected :get_line_port def controller_name self.class.name end def get_line_port_without_domain SipperConfigurator[:AgentLinePortWithoutDomain].to_s end protected :get_line_port_without_domain def get_auth_password SipperConfigurator[:AgentAuthenticationPassword] end protected :get_auth_password def get_auth_id SipperConfigurator[:AgentAuthenticationUserId] end protected :get_auth_id end
Now the rest is simple, you can add cucumber steps for like this :
Then customer Should get Alerting on his phone Then customer waits for 10 seconds Then customer accepts the phone Then customer waits for 20 seconds Then customer hangups the phone
and the steps definitions like this:
Then /^customer accepts the Call$/ do
puts "Agent is accepting the Call"
@customer_controller.accept_call()
end
Then /^customer hangs up the Call$/ do
puts "Agent is hanging up the Call"
@customer_controller.end_call()
end
Then /^customer should get alerting on his phone$/ do
i = 0
#wait for the ringing event to come
while ! @customer_controller.is_ringing() && i < 10
sleep 1
i += 1
end
if !@customer_controller.is_ringing()
raise "Agent didn't get any incoming call"
end
end
There you go. The beauty of this approach is that now, even your product owner can write test steps without having any clue of SIP / SIPPER. This is just plain english!
The "CommonCallController" I wrote is neither perfect not elegant. It uses instance variables to store the call state which can get wild for unexpected call flows. But It can easily be modified to suit your need. For me, it does its job for simple call flows.
There are plenty of documents on the web about how to use cucumber with Watir to automate webapplication testing..so I am not gonna talk about that.
Awesome. I think we should integrate Cucumber with the Sipr’s sgen to create testcase from plain english commands as posted in this article.
yes, that would be awesome. Also, sipper has a very good sip stack and call flow abstraction. You should consider providing programmatic api/library to that stack so that customers can use that framework from simple ruby test cases. It is not wise to always assume that the test cases will be always executed from sipper rake environment.