Test with several browsers at once using Selenium WebDriver

The majority of automated tests act on behalf of a single user during the entire time of test scenario. However there are the cases (and I wouldn’t say they are rare), which require checking how several users interact with each other (or how one user activity impacts another user’s state) in application under test. In today’s post we’ll look at a simple example of such behavior. The example as usual will cover a bit more than it requires for minimal demonstration. I do that in order to gradually develop not just Selenium API skills but also improve the skills or code organization and code architecture in general.

Nevertheless at the end I’ll provide the minimal representative example that would be free of different architectural things.

Example description

We start from example description as usual. In our example we’re going to open two browsers which are about to work in parallel. Both the browsers will start from opening different pages and will be performing "parallel" actions. I’m taking "parallel" in double-quotes since in fact the actions are not strictly parallel. That would require introducing multi-threading which would increase the code volume but wouldn’t add really much of understanding the concept. The actions will be taken sequentially but within two browsers working in parallel. Maybe later I will write the post where I will show the examples where true parallelization would be really necessary.

Those two browsers while working in parallel will go to some pages (each will go to its own) where we will check the page title (we’re testing after all..).

To clearly understand the code that will be provided in examples, it would be useful to get aware of interfaces, functional interfaces and lambda expressions

Prepare to test development

We’ll going to develop the test in Java language. In my example I’ll be using FireFox browser and Linux operating system. Also except of Selenium I’m going to use Maven and JUnit. If you are not yet familiar with these frameworks - it’s not a big issue. Conceptually they do not introduce value to the topic we’re discussing but they decrease the amount of work and amount of code, hence helping us to concentrate on really important things. In order to make us code access required libraries make sure that your pom.xml looks like the below:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>webelement-click</groupId>
    <artifactId>selenium-svg-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>3.141.59</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

Then download and place somewhere on your hard drive the webdriver for the operating system you use. At the moment I’m writing this post, Geckodriver is the official webdriver for FireFox browser. If you are working in Linux it will also worth making sure that the user on behalf of which you’re going to run your tests has the permission to execute webdriver executable.

Add a bit of architecture

When you start developing test automation project from scratch, it is useful to assess the amount and complexity of work you will have to do (number of tests, libraries that would be required, type and representation of incoming data, etc.) and think about the effective architecture in advance. When I say "effective architecture" I mean the organization of code that would allow you to easily add new and amend old tests. Let’s add a "Step" to our project. We’ll describe a step with the help of functional interface IStep.

package click.webelement.parallel;

import org.openqa.selenium.WebDriver;

public interface IStep {
    void doStep(WebDriver driver);
}

This interface defines the only single method doStep(), which we will use later in StepPerformer - the entity that will be responsible (due to my design) for scenario step execution. Lets look at the class implementation.

package click.webelement.parallel;

import org.openqa.selenium.WebDriver;

public class StepPerformer {

    private WebDriver driver;

    public StepPerformer(WebDriver driver){
        this.driver = driver;
    }

    public void executeStep(IStep step){
        step.doStep(driver);
    }

    public void terminate(){
        if(driver != null){
            driver.quit();
        }
    }

}

From the above code we can see that when one creates StepPerformer object they need to pass WebDriver object to the constructor. That object in turn is assigned to driver inner field. Except of constructor, the class also defines a method that is responsible for test step execution. We cannot know in advance what the step would exactly be. But we do know that the step would have to implement IStep interface. Hence within executeStep(IStep step) method we call doStep() method of step that we know would be present in the object that would be passed from somewhere outside in future. We also know that it would have to accept our driver object.

Hence the developers who will be develop the test steps will not care of where we take our driver from. They will just know that they will receive the proper driver from the code that is responsible for managing drivers. Such construction allows to add and remove the functionality that is common for all the steps. For example we can wrap our step execution with time measurement code or log the steps with adding only a few lines.

We also add terminate() method to the class that we’ll be using for releasing resources which have to be released on scenario completion.

Create a base for the test

Lets write a few lines of code to ensure the proper startup and proper shutting down of our tests. In the below fragment we (using JUnit annotations - read about JUnit basics) markup our code with special markers (annotations) so that JUnit framework is aware which code is responsible for what.

package click.webelement.parallel;

import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.firefox.FirefoxDriver;

public class TestParallelUsers {

    StepPerformer stepsOfOneUser;
    StepPerformer stepsOfAnotherUser;

    @BeforeAll
    public static void globalSetup(){
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Dev/webdrivers/geckodriver");
    }

    @BeforeEach
    public void setUp(){
        stepsOfOneUser = new StepPerformer(new FirefoxDriver());
        stepsOfAnotherUser = new StepPerformer(new FirefoxDriver());
        stepsOfOneUser.executeStep(d -> d.get("http://webelement.click/en/welcome"));
        stepsOfAnotherUser.executeStep(d -> d.get("http://webelement.click/en/selenium_and_svg_example_java"));
    }

    @AfterEach
    public void tearDown(){
        stepsOfOneUser.terminate();
        stepsOfAnotherUser.terminate();
    }

    @Test
    public void testTwoBrowsersSimultaneously(){
    }

    private void ezWait(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Lets look at some noticeable places which we have here in the class. First and the main one is that we define two field of StepPerformer types. One for each test user. Remember that in our architecture these entities will be responsible for scenario step execution. Then in globalSetup() method we specify the path to our webdriver executable (that will likely be different in your case). Before every test in our class will start (in the example there is only single test of course, however if we will decide to add more tests we’ll be ready for that) we open the initial pages for each of our two browsers.

Here we encounter the way we implement the steps in our architecture for the first time. In order to make you IDE handle such the syntax correctly you need to enable Java 1.8 syntax support in the preferences menu. The form of the expressions shown in the example is called a lambda-expression. We could implement the logic without lambdas but it would add much more lines to create anonymous objects instead of a short expression.

So, in setUp() before each particular test we perform the "step" which opens up the initial page for each user.

After each test we must ensure the proper shutting down and releasing our drivers not forgetting to test the driver references for nulls so that we would not encounter `NullPointerException`s.

In my example I will demonstrate only one test. In the implementation above it does not have any code in method body. We add it later. Yet, in ezWait() method we describe the logic of simple sleep that we need for the sake of demonstration. We take it out to the separate method in order to reuse it in our steps to to waste the space with duplication (since we call Thread.sleep() we have to somehow handle the exception which might be thrown on each of the calls).

Add the test

After we have prepared everything from architectural stand point we are ready to add test logic. So our the only test method will look like this:

@Test
public void testTwoBrowsersSimultaneously(){
    stepsOfOneUser.executeStep(d -> {
        d.findElement(By.xpath("//ul[@class='side-menu']/li[3]")).click();
        ezWait();
    });
    stepsOfAnotherUser.executeStep(d -> {
        d.findElement(By.xpath("//div[@id='menu-tags']/div[1]/a")).click();
        ezWait();
    });
    stepsOfOneUser.executeStep(d -> Assertions.assertTrue(d.getTitle().contains("Feedback")));
    stepsOfAnotherUser.executeStep(d -> Assertions.assertTrue(d.getTitle().contains("Search")));
}

Despite the one-line lambda-expression highly increases readability of the code the multi-line expressions are still allowed. We will use them since we need to pause the code for demo purposes.

In general, the complex steps (consisting of several sub-steps) are quite common thing. The suggested design allows the developers to implement their own steps which can be stored as implementations of IStep interface in the codebase and then reused where required. I will cover this topic in later posts.

Clear the example from "architecture"

If you do not need any architectural knowledge and just want to see how the example would look like with only Selenium and assertions, below you can see the minimal working example:

package click.webelement.parallel;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class TestParallelUsers {

    @Test
    public void minimalExample(){
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Dev/webdrivers/geckodriver");
        WebDriver user1 = new FirefoxDriver();
        WebDriver user2 = new FirefoxDriver();
        user1.get("http://webelement.click/en/welcome");
        user2.get("http://webelement.click/en/selenium_and_svg_example_java");
        user1.findElement(By.xpath("//ul[@class='side-menu']/li[3]")).click();
        user2.findElement(By.xpath("//div[@id='menu-tags']/div[1]/a")).click();
        Assertions.assertTrue(user1.getTitle().contains("Feedback"));
        Assertions.assertTrue(user2.getTitle().contains("Search"));
        user1.quit();
        user2.quit();
    }

}

If you still have the questions please send them to me using this form. I will amend the article according to your feedback.