Skip to main content

_ruby

A brief primer on Good Test Writing

In order to write tests that work well in parallel there are some simple guidelines to follow:

  • Write atomic and autonomous tests
  • Group like tests together in small batches
  • Be descriptive
  • Use a Test Runner

Atomic & Autonomous Tests

Each test needs to be concise (e.g. testing a single feature rather than multiple features) and be able to be run independently (e.g. sets up it's own data rather than relying on a previous test to do it). Doing this may require a mental shift, discipline, and more up front effort. But it will make a dramatic impact on the quality, effectiveness, and maintainability of your tests.

Grouping Tests

As your test suite grows you should have multiple test files, each containing a small grouping of tests broken out by functionality that they're exercising. This will go a long way towards organization and maintenance as your test suite grows -- as well as faster execution times (depending on your approach to parallelization).

Being Descriptive

Each test file should be named appropriately, and each test within it should have an informative name (even if it may be a bit verbose). Also, each test (or grouping of tests) should be tagged for flexible execution later (e.g. on a Continuous Integration server).

This way you can run parts of your test suite as needed, and the results will be informative thanks to helpful naming.

Test Runners

At the heart of every testing framework is a test runner that does a lot of the heavy lifting (e.g. test group execution, easy global configuration for setup and teardown, reporting, etc.).

So rather than reinvent the wheel, you can use one of the many that already exists (there's more than one for every language). And with it, you can bolt on third party libraries to extend it's functionality if there's something missing -- like parallelization.

Example

NOTE: For this example we will be using RSpec as the Test Runner and parallel-tests for parallelization.

In RSpec, test files are referred to as 'specs'. So for our initial spec I've used the test code from a previous tip on working with Data Tables. Don't get too distracted by what the test steps are doing, the organization and naming of things is more important for this example.

# data_table_sorting_spec.rb

require 'selenium-webdriver'

describe 'Data Table Sorting' do

before(:each) do
@driver = Selenium::WebDriver.for :firefox
end

after(:each) do
@driver.quit
end

context 'Without Attributes' do

it 'in Ascending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_i }
(due_values == due_values.sort).should == true
end

it 'in Descending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_i }
(due_values == due_values.sort).should == false
end

end

context 'With Attributes' do

it 'in Ascending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table2 thead .dues').click
dues = @driver.find_elements(css: '#table2 tbody .dues')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_f }
(due_values == due_values.sort).should == true
end

it 'in Descending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table2 thead .dues').click
@driver.find_element(css: '#table2 thead .dues').click
dues = @driver.find_elements(css: '#table2 tbody .dues')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_f }
(due_values == due_values.sort).should == false
end

end

end

At the top of the file we include our dependent library (selenium-webdriver) and declare the name of our test group ('Sort Data Table') with a describe statement. We then wire up our before and after actions which are responsible for setting up and tearing down an instance of Selenium for each test. Each of our tests are grouped into logical separations using context blocks and defined within an it block.

If we run this (e.g. with the command rspec data_table_sorting_spec.rb) it will fire up one browser at a time until each test is completed. And If we download, install, and run it through parallel_tests (e.g. with the command paralell_rspec data_table_sorting_spec.rb) -- the same thing would happen. Why? Because parallel_tests executes things at the file level rather than at the individual test level (this holds true even when running tests by tag).

In order to take advantage of parallelization we will need to break these tests up into different files.

But before we do that we can simplify our test code by abstracting out our setup and teardown actions into a central place (known as a spec_helper in the RSpec parlance).

# spec_helper.rb

require 'selenium-webdriver'

RSpec.configure do |config|

config.before(:each) do
@driver = Selenium::WebDriver.for :firefox
end

config.after(:each) do
@driver.quit
end

end

Now that we have that we can create a new spec file and place some of our tests in it.

# data_table_sorting_with_attributes_spec.rb

require 'spec_helper'

describe 'Sort Data Table' do

context 'With Attributes' do

it 'in Ascending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table2 thead .dues').click
dues = @driver.find_elements(css: '#table2 tbody .dues')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_f }
(due_values == due_values.sort).should == true
end

it 'in Descending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table2 thead .dues').click
@driver.find_element(css: '#table2 thead .dues').click
dues = @driver.find_elements(css: '#table2 tbody .dues')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_f }
(due_values == due_values.sort).should == false
end

end

end

Notice that the require statement has changed from selenium-webdriver to spec_helper. Since we abstracted things out into a spec_helper file we now need to require it in all of our specs -- and the dependent libraries for our test suite live there instead.

# data_table_sorting_without_attributes_spec.rb

require 'spec_helper'

describe 'Sort Data Table' do

context 'Without Attributes' do

it 'in Ascending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_i }
(due_values == due_values.sort).should == true
end

it 'in Descending Order' do
@driver.get 'http://the-internet.herokuapp.com/tables'
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
@driver.find_element(css: '#table1 thead tr th:nth-of-type(4)').click
dues = @driver.find_elements(css: '#table1 tbody tr td:nth-of-type(4)')
due_values = []
dues.each { |due| due_values << due.text.gsub(/\$/,'').to_i }
(due_values == due_values.sort).should == false
end

end

end

To get this last file we could have renamed the original test file, or created it anew and deleted the original.

Notice that the describe name is the same between test files (e.g. 'Sort Data Table'). In RSpec names do not need to be unique, and these tests logically fit together, this is a welcome consistency.

Now when we run our tests through parallel_tests we get concurrent test runs with two browsers firing at the same time, yielding (in this case) roughly a 30% drop in execution times (e.g. 17 seconds down to 12 seconds). You can easily up the number of processes being run as a command-line argument (e.g. -n 5). But this will only make a difference as you have more specs given how parallel_tests groups and executes things.

Now that we have a working example that runs concurrently, we can point it at a cloud offering like Sauce Labs, and have access to any browser we want. We just have to update our setup action in the spec_helper.

NOTE: This was covered in a previous tip. For a full write-up, go here.

# spec_helper.rb

require 'selenium-webdriver'

RSpec.configure do |config|

config.before(:each) do
caps = Selenium::WebDriver::Remote::Capabilities.firefox
caps.browser_version = "115"
caps.platform_name = "Windows 11"
caps[:name] = self.example.metadata[:full_description]

@driver = Selenium::WebDriver.for(
:remote,
:url => "https://SAUCE_USERNAME:SAUCE_API_KEY@ondemand.saucelabs.com/wd/hub",
:capabilities => caps)
end

config.after(:each) do
@driver.quit
end

end

Now when we run our tests with parallel_tests we can see the concurrent execution happening in Sauce Labs.

Sauce Labs is built to handle your tests concurrently, so you should be able to turn the number of processes up with no sweat. You will just need to be cogniscent of how many parallel tests your account has access to (e.g. 3 for Sauce Open accounts).

Expected Behavior

  • Several browsers open in parallel
  • Each test runs in a different browser
  • All browsers close
  • If Sauce Labs was used, then the test results (along with a video recording, screenshots, and other debugging information) are available on the test results dashboard.

Summary

Hopefully this tip has helped steer you on a path towards better test writing and parallelization.

Stay tuned for future tips where we'll cover how to take full advantage of your test suite by wiring it into a Continuous Integration server and making the requisite changes to your test runner.

Happy Testing!