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.