Integrate Sipper with Cucumber for functional automation

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.

2 thoughts on “Integrate Sipper with Cucumber for functional automation

    • 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s