Parallel Execution with Selenium Grid and Docker

Posted by Omar Ortega on Wednesday, February 19, 2025

In my opinion Parallel Execution is one of the crucial features we could have in an Automation Framework, why?… because it enables the way we are going to execute our tests, and the velocity on getting results on the Test Suite.

Selenium Grid

Why using selenium grid?, well first of all it is because the team that is developing and maintaining it, is the same team developing the Selenium automation library. Another important point is that, we can use Docker to spin up the Selenium Grid and be ready to send commands, so lets see how we can do this.

Docker Compose

Docker compose now ships with Docker engine and you no longer need to install it separately, if you don’t know how to install docker in your Operative System, you can visit this web site Docker Install.

Docker Compose Files

Compose uses YAML files to define micro services applications. The default name is docker-compose.yml or you can use the -f flag to specify custom file names.

For our specific case, we are going to use the docker compose file provided by the Selenium Grid team, we will modify it a bit to meet our requirements but overall you can visit the Selenium Grid GH Repository. The file we are going to use setups a Full Selenium Grid, that contains a Selenium Distributor and Selenium Router, these will help us on having a load balancing process for our tests.

Create a file named docker-compose.yml at the root of your project and add the next code lines.

version: "3"
services:
  selenium-event-bus:
    image: selenium/event-bus:4.28.1-20250202
    container_name: selenium-event-bus
    ports:
      - "4442:4442"
      - "4443:4443"
      - "5557:5557"

  selenium-sessions:
    image: selenium/sessions:4.28.1-20250202
    container_name: selenium-sessions
    ports:
      - "5556:5556"
    depends_on:
      - selenium-event-bus
    environment:
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true
      - SE_NODE_MAX_SESSIONS=5
      - SE_EVENT_BUS_HOST=selenium-event-bus
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

  selenium-session-queue:
    image: selenium/session-queue:4.28.1-20250202
    container_name: selenium-session-queue
    ports:
      - "5559:5559"

  selenium-distributor:
    image: selenium/distributor:4.28.1-20250202
    container_name: selenium-distributor
    ports:
      - "5553:5553"
    depends_on:
      - selenium-event-bus
      - selenium-sessions
      - selenium-session-queue
    environment:
      - SE_EVENT_BUS_HOST=selenium-event-bus
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_SESSIONS_MAP_HOST=selenium-sessions
      - SE_SESSIONS_MAP_PORT=5556
      - SE_SESSION_QUEUE_HOST=selenium-session-queue
      - SE_SESSION_QUEUE_PORT=5559

  selenium-router:
    image: selenium/router:4.28.1-20250202
    container_name: selenium-router
    ports:
      - "4444:4444"
    depends_on:
      - selenium-distributor
      - selenium-sessions
      - selenium-session-queue
    environment:
      - SE_DISTRIBUTOR_HOST=selenium-distributor
      - SE_DISTRIBUTOR_PORT=5553
      - SE_SESSIONS_MAP_HOST=selenium-sessions
      - SE_SESSIONS_MAP_PORT=5556
      - SE_SESSION_QUEUE_HOST=selenium-session-queue
      - SE_SESSION_QUEUE_PORT=5559

  chrome:
    image: selenium/node-chrome:4.28.1-20250202
    shm_size: 2gb
    depends_on:
      - selenium-event-bus
    environment:
      - SE_NODE_OVERRIDE_MAX_SESSIONS=true
      - SE_NODE_MAX_SESSIONS=5
      - SE_EVENT_BUS_HOST=selenium-event-bus
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

  edge:
    image: selenium/node-edge:4.28.1-20250202
    shm_size: 2gb
    depends_on:
      - selenium-event-bus
    environment:
      - SE_EVENT_BUS_HOST=selenium-event-bus
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

  firefox:
    image: selenium/node-firefox:4.28.1-20250202
    shm_size: 2gb
    depends_on:
      - selenium-event-bus
    environment:
      - SE_EVENT_BUS_HOST=selenium-event-bus
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

We will use 3 different browsers: chrome, firefox and ms edge.

We would be adding for now 2 options to the chrome setup, these options are related to the Max number of sessions we can concurrently execute on chrome browser. You do not need to worry about modifying the above code, those instructions are already set.

- SE_NODE_OVERRIDE_MAX_SESSIONS=true
- SE_NODE_MAX_SESSIONS=5

Those two lines of code will tell the Grid that we are going to enable the MAX SESSION option, and that we need that 5 is the number.

Deploying the Selenium Grid with Compose

Now that we have our setup in place it’s time to deploy the grid, to do so, it’s super simple, we will use Docker Compose commands to deploy it.

docker compose up is the most common way to bring up a Compose app. It pulls all the required images, creates all the networks and volumes, and starts the containers.

docker compose down will bring the Compose app down, all the containers and networks are deleted by default.

By using the docker compose upcommand we can now have our Selenium Grid deployed, we can access it by navigating to Selenium Grid .

This uses the endpoint http:\\localhost:4444

Image Description

We are ready to run our tests! 😎

Adding Executor Command line option

In the previous blog posts we added a command line option to select the browser on the go (Chrome or Firefox), Selecting a web browser from command line, We will add another option to select the type of Test Execution we want, like local or grid.

We are going to start by adding a new class in the config.py file, this CLI option will request you to pass 2 possible options: local or grid.

class Config_execution:
    def __init__(self, execution):

        SUPPORTED_EXECUTORS = ['local', 'grid']

        if execution == None or execution.lower() not in SUPPORTED_EXECUTORS:
            raise Exception(f'{execution} please select an option from the Supported Executors with the option --execution (supported browsers: {SUPPORTED_EXECUTORS})')


        self.execution_type = {
                'grid': 'grid',
                'local': 'local'
        }[execution]

Now that we have the new CLI option added we can now work on adding the fixture to execute our test locally or on the Selenium Grid.

Execution Fixture

We will add 2 new fixtures to our conftest.py file, those are going to help us to read the option from the --executor CLI option, and store it.

@fixture(scope="function")
def execution_type(request):
    return request.config.getoption("--execution")

Now the one that is going to setup our webdriver as to be running locally or remotelly:

@fixture(scope='function')
def execution(execution_type, browser_type):
    cfg = Config_browser(browser_type).browser_type
    efg = Config_execution(execution_type).execution_type
    try:
        match efg:
            case "local":
                if cfg == "chrome_browser":
                    driver = webdriver.Chrome()
                elif cfg == "firefox_browser":
                    driver = webdriver.Firefox()
            case "grid":
                selenium_grid_config = {
                    'browserName': browser_type,
                }
                options = {
                    "chrome_browser": webdriver.ChromeOptions(),
                    "firefox_browser": webdriver.FirefoxOptions()
                }.get(cfg)
                driver = webdriver.Remote(
                    command_executor='http://localhost:4444',
                    options=options
                )
                print(options)
        yield driver
    finally:
        if driver:
            driver.quit()

This fixture will read the two options --browser and --execution, why we are ready the --browser too? , because we need to setup the browser type, in the Selenium Grid we have several options, so we need to indicate to the webdriver on which browser we will perform our tests.

We will setup the two cases we have, the local one will set up the driver as Chrome or as Firefox to be executed locally. The grid one will then setup the webdriver option, depending on the selected browser, and then it will setup the driver as webdriver.Remote this will indicate that we are going to use a different endpoint and port, and because we are running our Selenium Grid on our computers, the default endpoint is http:\\localhost:4444, if we have our Selenium Grid deployed in an external server, that endpoint will change.

Now the final step, we now need to pass our driver to our tests, because we are now setting up the driver with a different fixture, the tests cases need to be updated to get the execution one, instead of the browser, we update the tests as:

def test_home_page_loaded(execution):

    training_page = AB_TESTING(driver=execution)
    training_page.go()
    training_page.ab_testing_link.click()
    header_text = training_page.ab_testing_header.text
    assert "A/B Test Control" in header_text

We already accomplished a lot!!, but… how are we going to run our test in parallel?

Setting up Pytest Parallel Execution

To accomplish Parellel execution either locally or on the grid, we need another python dependency pytest-xdist.

pytest-xdist is a plugin that extends pytest with new test execution modes, the most used being distributing tests accross multiple CPUs to speed up test execution.

To install the dependency:

pip install pytest-xdist

It is time to set up pytest-xcdist because we need to setup the maximum number of workers that we can process. To do this we will add the next code lines into our pytest.ini file:

[xdist-parallel]
num_workers = 4  # Set the number of worker processes to use

I chose 4 because that is the number of processes that I want to use, and it will depend on the CPU you have. To know more about xdist, you can visit their web site pytest-xdist

Executing Tests

Now the moment we’ve all be waiting for, time to execute our tests.

We will have three new CLI options to run --browser, execution, and --numprocesses this can be abbreviated to -n.

Lets first test our local setup:

pytest --numprocesses 2 --browser chrome --execution local

Pytest will trigger the execution and you should see different chrome browsers poping up, this means we have local parallel execution! 😱

Now Selenium Grid:

pytest -n 2 --browser chrome --execution grid

Pytest is going to send the commands to the selenium grid, you can now see Selenium Grid orchestrating the tests automatically! 🚀

This is awesome we now have different ways to execute our tests and the framework is capable of receiving different CLI options to meet our needs.

Thank you for getting this far in this blog post, please check the code in the link: Pytest Selenium GH Project