Running Selenium Tests in parallel with GitHub Actions

Running Selenium Tests in Parallel with GitHub Actions

How to run Selenium tests in parallel with GitHub Actions

Introduction

In our ongoing quest for efficiency and modernisation, our company recently moved from using Teamcity to using GitHub Actions for our Continuous Integration (CI) processes. This significant change brought with it a number of challenges, particularly for the Quality Assurance (QA) team. As QAs, we were faced with the critical task of adapting our test automation suites to integrate with the new workflow. This included a complex suite that used a mixture of Geb, Selenium, TestNG and Allure and was dedicated solely to UI testing. This transition wasn't just a change of tools. It was a strategic move towards more agile, collaborative and efficient development practices, which led us to rethink and refine our approach to software testing.

Beginning

Our journey started with the initial configuration based on the standard implementation - Selenium hub and nodes (Firefox and Chrome). The whole environment was running on a specific Teamcity agent (virtual machine) that was pre-configured. By preconfigured I mean that all Docker images were stored on this agent, so there was no need to download them. Also, compared to other Teamcity agents, our Teamcity Selenium agents had more RAM to handle running parallel tests. So the whole process was quite simple:

We ran all the tests through the TestNg test group option. In each test we added specific groups such as

@Test(groups = ["apples"])
Running Selenium tests - Teamcity
Running Selenium tests - Teamcity

Performance: ~11 min,

Debugging: Hard (need to rerun all the suite)

Approach 1: Lets do the same

We started by adapting the test automation suite to GitHub Actions with a simple approach. Do the same configuration as in Teamcity.

Right from the start we noticed some problems. First, we couldn't have pre-installed Docker images on the workers (for security reasons), so we had to download the image each time. The second problem was when we wanted to scale nodes. We couldn't use Docker Compose to set up the Selenium hub and nodes. Scaling this way wasn't very good either. The GitHub Actions workers were much smaller than the Teamcity workers. Performance dropped significantly when scaling to 6 separate nodes.

Run Selenium tests on GitHub Actions - first approach
Run Selenium tests on GitHub Actions - first approach

We also tried to change RAM size but we couldn't directly manipulate the 'power' of runners (workers). They had strict RAM policy.

Performance: ~19 min

Debugging: Hard (need to rerun all the suite)

Approach 2: Lets run every test per worker

The problem with the previous approach was mainly performance. We were experiencing a significant drop in performance (almost 72%, from 11 minutes to 19 minutes). It was at this point that we needed to focus on improving performance.

And a new problem appeared. How to run tests in parallel with GitHub Actions? Previously we were running tests through TestNg group option and Selenium hub & nodes. But how to run Selenium tests in parallel without this architecture. We would need to split tests in previous steps somehow and then pass list of tests to next jobs.

We would need something like this:

We decided to run each test on a separate worker. First, we need a simple shell script to get a list of all available tests from the test group (directory in this case):

declare -a array=(./src/test/groovy/com/airhelp/geb/tests/$1/*)

count=0
for i in ${array[@]}
do
  array[$count]=$(sed 's/.\/src\/test\/groovy//g;s/.groovy//g;s#/#.#g;;s#.com#com#g;' <<< $i)
  count=$((count + 1))
done

printf '%s\n' "${array[@]}" | jq -R . | jq -cs .

What does this script do?

As a parameter, it takes the test path to the directory with tests. So if we run it on the test directory 'apples', it should return a list of all the tests in the apples directory.

Lets present now the part of preparation workflow:

preparation-tests:
  runs-on: [self-hosted, utils]
  steps:
    - name: Checkout tests repo
      uses: actions/checkout@v3

    - name: List all tests
      id: set-matrix
      run: |
        echo "TESTS=$(bash get_list_of_test.sh ${{ inputs.testPath }})" >> $GITHUB_OUTPUT
    - name: Get tests
      run: echo "The selected tests are ${{ steps.set-matrix.outputs.TESTS }}"
  outputs:
    matrix: ${{ steps.set-matrix.outputs.TESTS }}

In next steps we will use the output from this step.

- name: Run tests with test path
  shell: bash
  run: |
    docker run --rm -u 0\
    --network=host \
    -v $(pwd):/tests -w /tests \
    -Dah.max_retries=2 -Dah_hub_port=4444 -DmaxParallelForks=1 \
    -DtestPath=${{ matrix.test-path }} -s clean remoteTest
Run Selenium tests on GitHub Actions - second approach
Run Selenium tests on GitHub Actions - second approach

Succeed! We run parallel tests even faster than before. But a new problem has arisen.

We have 30 tests. For each test we need a separate worker. This means that for 30 tests we would need 30 workers. It's quite a big effort.

Performance: ~7 mins

Debugging: Good (after fail we can rerun specific test not whole test suite)

Approach 3: multi groups

The final approach is to use subgroups and it looks like this:

{
  "test_group": ["documents_1", "documents_2"]
}

And how does it look step by step in the GitHub Actions flow?

on:
  workflow_call:
    inputs:
      includeGroups:
        type: string
        description: "includeGroups"
        required: true
  workflow_dispatch:
    inputs:
      includeGroups:
        description: "includeGroups"
        type: choice
        required: true
        default: "default_group"
        options:
          - documents
preparation-tests:
  runs-on: [self-hosted, tests]
  steps:
    - name: Checkout tests repo
      uses: actions/checkout@v3

    - name: List test groups
      id: set-matrix
      run: |
        echo "matrix=$(sed -e 's/^ *//' < ./tests/${{ inputs.includeGroups || 'default_group' }}.json | tr -d '\n')" >> $GITHUB_OUTPUT
    - name: Get tests
      run: echo "The selected tests are ${{ steps.set-matrix.outputs.matrix }}"
  outputs:
    matrix: ${{ steps.set-matrix.outputs.matrix }}
selenium:
  name: Run Selenium tests for group - ${{ inputs.includeGroups }}
  runs-on: [self-hosted, tests]
  timeout-minutes: 120

  needs: [preparation-tests]
  strategy:
    fail-fast: false
    matrix:
      test-group: ${{fromJson(needs.preparation-tests.outputs.matrix).test_group}}
- name: Run tests with test group - ${{ matrix.test-group }}
  shell: bash
  run: |
    docker run --rm -u 0\
    --network=host \
    -v $(pwd):/tests -w /tests \
    -Dah.max_retries=2 -Dah_hub_port=4444 -DmaxParallelForks=1 \
    -DincludeGroups=${{ matrix.test-group }} -s clean remoteTest
Run Selenium tests on GitHub Actions - multi group approach
Run Selenium tests on GitHub Actions - multi group approach

There are also some drawbacks to this approach:

Performance: ~9 min

Debugging: Medium (need to rerun specific subgroups with tests)

Summary

There are no perfect ways to make parallel testing in GitHub Actions, as it greatly depends on individual project requirements, the specifics of the current infrastructure setup, security considerations, and budgetary constraints. This variability means that each project may require a unique approach to effectively implement parallel testing.

Modern automation frameworks like Playwright and Cypress are addressing these varied needs. Playwright lets developers run multiple tests at once, which makes everything faster. Cypress has a special feature that spreads out tests evenly across different machines so that no single machine is overwhelmed.

Despite the growing popularity of Cypress and Playwright, Selenium remains a top used in the world of test automation frameworks. Also it's important to remember how important it is to get quick feedback on the state of our software. Therefore, we should aim to set up Selenium tests to run in parallel to get feedback as soon as possible. Fortunately, with GitHub Actions, there's a way to do this using matrix strategy, which allows us to run Selenium tests in parallel, efficiently addressing the need for fast testing and reliable results.

Mateusz Banasik's Profile

Mateusz Banasik

Quality Assurance Engineer