Standard tickets for SeleniumConf Berlin are on sale NOW (while supplies last)! For details GO HERE x

48

How To Do Load Testing

The Problem

One of the most commonly asked questions in Selenium is "How do you do load testing?". The often stated short answer is "Well... you don't".

There are plenty of resources citing that while it can be done there are better tools for the job. Tools like JMeter are often recommended. But they can be intimidating and challenging to get started with.

If you start to go down this rabbit hole then there are a couple of options. You could stand up a Selenium Grid with a bunch of host nodes, run a big suite of tests in parallel, and hope for the best. You could also do this with a cloud provider (instead of maintaining your own Grid). But these are hard enough to get right when you're just trying to get your automated tests working well in the first place. Alternatively, you could go with a third-party provider like Neustar to help you on your way, but that might not be your cup of tea.

Regardless of the approach, it can be a bumpy road to do load testing.

A Solution

Rather than invest time, money, and frustration into JMeter, a Selenium infrastructure, or a third-party provider, we can generate a good amount of load by leveraging an existing Selenium script, a proxy server, and an HTTP client library along with some parallel threads.

With a proxy server we can capture the HTTP requests generated by Selenium and replay them with an HTTP client library in parallel -- effectively replicating the same actions hitting the web server simultaneously, in large numbers.

Let's dig in with an example.

An Example

Before we get started we'll need to download a copy of BrowserMob Proxy. Once we have that, we'll want to include our requisite libraries:

  • selenium-webdriver to control the browser
  • browsermob/proxy to configure/use BrowserMob Proxy
  • typhoeus to replay HTTP requests

After that, we can create methods to configure the proxy server (configure_proxy), set the browser profile to use the proxy server (browser_profile), pull these together so the test will have a working browser that uses the proxy server (setup), and tear things down after running the test (teardown).

# filename: load.rb

require 'selenium-webdriver'
require 'browsermob/proxy'
require 'typhoeus'

def configure_proxy
  proxy_binary = BrowserMob::Proxy::Server.new('./browsermob-proxy/bin/browsermob-proxy')
  proxy_binary.start
  proxy_binary.create_proxy
end

def browser_profile
  browser_profile = Selenium::WebDriver::Firefox::Profile.new
  browser_profile.proxy = @proxy.selenium_proxy
  browser_profile
end

def setup
  @proxy = configure_proxy
  @driver = Selenium::WebDriver.for :firefox, profile: browser_profile
end

def teardown
  @driver.quit
  @proxy.close
end

Next we'll want to tell the proxy server to capture traffic and return a payload (a.k.a. a HTTP Archive, or HAR). For easier debugging later, we will want to add some error handling to output the results from each HTTP request that's replayed (error_handling).

def capture_traffic
  @proxy.new_har
  yield
  @proxy.har
end

def error_handling(request)
  request.on_complete do |response|
    if response.success?
      puts "success"
    elsif response.timed_out?
      puts "got a time out"
    elsif response.code == 0
      # Could not get an http response, something's wrong.
      puts response.return_message
    else
      # Received a non-successful http response.
      puts "HTTP request failed: " + response.code.to_s
    end
  end
end

Now for the meat of the issue -- replaying the requests.

def replay(http_requests, number_of_replays, debug = false)
  requests = []
  http_requests.entries.each do |entry|
    requests << Typhoeus::Request.new(
      entry.request.url,
      method: entry.request.method.downcase.to_sym)
  end

  start_time = Time.now
  puts "Start time: #{start_time}"
  threads = []
  number_of_replays.times do
    threads << Thread.new do
      requests.each do |request|
        error_handling request if debug
        request.run
      end
    end
  end
  threads.each {|thread| thread.join }
  finish_time = Time.now
  puts "Finish time: #{finish_time}"
  puts "#{number_of_replays} runs completed in #{finish_time - start_time} seconds"
end

We start by iterating through each of the entries in the HAR payload, creating a new request for Typhoeus to use -- storing them all in an array.

After printing the current time to the terminal, we create a new thread and execute all of the Typhoeus HTTP requests within it (outputting the error handling for each request if a debug parameter is passed in). This gets repeated based on the number of times specified when replay is called.

After the threads run and terminate, we output the current time, and then list the total time run time along with the number of replays attempted.

We can now pull all of this together by implementing a simple run method.

def run
  setup
  http_requests = capture_traffic { yield }
  teardown
  replay(http_requests, 100)
end

If we wanted to see the debug output, we would pass in true as a third parameter like so:

replay(http_requests, 100, true)

Now that everything's wired up, we can drop our Selenium actions into a run block.

run do
  @driver.get 'http://the-internet.herokuapp.com/dynamic_loading/2'
  @driver.find_element(css: '#start button').click
  Selenium::WebDriver::Wait.new(timeout: 8).until do
    @driver.find_element(css: '#finish')
  end
end

If we run this script without debugging turned on, then we should see something like this:

Start time: 2014-04-30 14:22:33 -0400
Finish time: 2014-04-30 14:23:10 -0400
100 runs completed in 37.575052 seconds

Expected Behavior

  • Load a browser
  • Visit the page and complete the actions specified while capturing the HTTP requests
  • Close the browser and shutdown the proxy server
  • Replay the HTTP requests generated by Selenium 100 times in parallel

Back to the archives