Automated End-To-End Testing with Selenium and Docker

Automated End-To-End Testing with Selenium and Docker

Automated end to end testing is the most effective way to test your app. This tutorial explains how to use Selenium and Docker for End-to-End Testing. The Ultimate Guide to End to End tests with Selenium and Docker. Automated End-To-End Testing with Selenium and Docker

Automated end to end testing is the most effective way to test your app. It also requires the least effort to get the benefit of tests if you currently have no tests at all. And you don’t need a ton of infrastructure or cloud servers to get there. In this guide we’ll see how we can easily overcome the two main hurdles of end to end testing.

The first hurdle is Selenium. The API you use to write tests is ugly and non-intuitive. But it is not that difficult to use, and with a few convenient functions it can become a breeze to write Selenium tests. The effort is well rewarded because you can automatically test your end users' flows end to end.

The second hurdle is putting components together in an isolated environment. We want the frontend, the backend, the database and everything else your app uses. We will use Docker compose to put things together and automate the tests. It is easy even if you have your components in different Git repositories.

Writing end to end tests in Selenium

Even if you are an API only business, you have a frontend, and an admin or back office frontend. So end to end tests ultimately talk to a frontend application.
The industry standard tool is Selenium. Selenium provides an API to talk to the web browser and interact with the DOM. You can check what elements are displayed, fill inputs and click around. Anything a real user would do with your application, you can automate.

Selenium uses something called the WebDriver API. It is not very handy to use at first glance. But the learning curve is not steep. Creating a few convenience functions will get you productive in no time. I won’t go into the details of the WebDriver API here.

There are also libraries to make your life easier. Nightwatch is the most popular.

If you have an Angular application, Protractor is your best friend. It integrates with the Angular event loop and allows you to use selectors based on your model. That is gold.

Writing a test for your most critical user feature or your app should take only a few hours, so go ahead. It will run automatically ever after. Let's see how.

Running your tests in Docker

We need to run our tests in an isolated environment so the outcome is predictable. And so we can enable Continuous Integration easily. We'll use Docker compose.

Selenium provides Docker images out of the box to test with one or several browsers. The images spawn a Selenium server and a browser underneath. It can work with different browsers.

Let’s start with one browser for now: headless-chrome. The docker-compose.yml file looks as below (commands are from an Angular example).

Note: If you've never used Docker you can easily install it on your computer. Docker has the troublesome tendency of forcing you to sign up for an account just to download the thing. But you actually don't have to. Go to the release notes (link for Windows and link for Mac) and download not the latest version but the one right before. This is a direct download link.

version: '3.1'

   build: .
   image: myapp
   command: npm run serve:production
    - 4200

   image: myapp
   command: dockerize -wait tcp://app-serve:4200 
             -wait tcp://selenium-chrome-standalone:4444 
             -timeout 10s -wait-retry-interval 1s bash -c "npm run e2e"
     - app-serve
     - selenium-chrome-standalone

   image: selenium/standalone-chrome
    - 44444

The file above tells Docker to spin up an environment with 3 containers:

  • Our app to test: the container uses the myapp image which we’ll build right below
  • A container running the tests: the container uses the same myapp image. It uses dockerize to wait for the servers to be up before running the tests
  • The Selenium server: the container uses the official Selenium image. Nothing to do here. We could run the tests from the same container as the app. Splitting it makes things more clear. It also allows you to separate outputs from the 2 containers in the result logs.

The containers will live inside a private virtual network and see each other as http://the-container-name (more here on networking in Docker).

We need a Dockerfile to build the myapp image used for the containers. It should turn your frontend code into a bundle as close to production as possible. Running unit tests and linting is a good idea at that stage. After all no need to run end to end tests if the basics do not work.

In the Dockerfile below we use a node image as base, install dockerize and bundle the application. It is important to build for production. You don’t want to test a development build that is pre-optimizations. Many things can go wrong there.

FROM node:12.13.0 AS base


RUN wget$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
   && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
   && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

RUN mkdir -p ~/app

COPY package.json .
COPY package-lock.json .

FROM base AS dependencies

RUN npm install

FROM dependencies AS runtime

COPY . .
RUN npm run lint
RUN npm run test:ci
RUN npm run build --production

Now that we have all the pieces together let's run the tests using this command:

docker-compose up --build --abort-on-container-exit

It is long-ish, so script it in your project somehow. What it will do is build the myapp image based on the provided Dockerfile and then start all the containers. Dockerize makes sure your app and Selenium are up before executing the tests.

The --abort-on-container-exit option will kill the environment when one container exists. Since only our testing container is meant to exit (the others are servers), that is what we want.

The docker-compose command will have the same exit code as the exiting container. It means you can easily detect from the command line if the tests succeeded or not.

You are now ready to run end to end tests locally and on any server supporting Docker. That's pretty good!

Tests run with only one browser for now, though. Let’s add more.

Testing on different browsers

The standalone Selenium image spins up a Selenium server with the browser you want. To run the tests on different browsers you need to update your tests' configuration and use the selenium/hub Docker image.

The image creates a hub between your application and the standalone Selenium images. Replace the selenium container section in your docker-compose.yml as follows:

    image: selenium/hub
    container_name: selenium-hub
      - 4444
    image: selenium/node-chrome
      - /dev/shm:/dev/shm
      - selenium-hub
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444
    image: selenium/node-firefox
      - /dev/shm:/dev/shm
      - selenium-hub
      - HUB_HOST=selenium-hub
      - HUB_PORT=4444

We now have 3 containers: Chrome, Firefox and the Selenium hub.

All the Docker images provided by Selenium are in this repository.

Careful! There is a tricky timing effect to consider. We use dockerize to have our test container wait for the Selenium hub to be up. That is not enough because we need to wait for the standalone images to be ready – in fact to register themselves to the hub.

We can do this by waiting for the standalone images to expose a port but that is not a guarantee. It is easier to wait a few seconds before starting the tests. Update your test script to wait 5 seconds before the tests start (add a sleep command after the dockerize call).

Now you can be sure that your tests won’t start until all the browser are ready. Waiting is not ideal but a few seconds are worth it. There is nothing more annoying than failing tests because of unstable automations.

Good. We have now covered the frontend part. Let’s add the backend.

Add the backend as containers or git modules

The above might seem overkill to test only the front end part of your app. But we're aiming for much more. We want to test the whole system.

Let’s add a database and a backend to our Docker compose environment.

If you are a front end developer you might think "we are the frontend team we don’t care about testing the backend". Are you sure?

The frontend is always the last part to integrate all the other pieces. That means crunch time. Crunch time that would no longer be if you were able to test the frontend with the backend continuously and catch errors sooner.

The technique I describe here is very easy to apply even if your backend is in a different repository.

This is what the docker-compose.yml looks like:

version: '3.1'

   image: postgres
     POSTGRES_USER: john
     POSTGRES_PASSWORD: mysecretpassword
     - 5432
   context: ./backend
   dockerfile: ./backend/Dockerfile
   command: dockerize
       -wait tcp://db:5432 -timeout 10s
       bash -c "./ && ./"
     APP_DB_HOST: db
     APP_DB_USER: john
     APP_DB_PASSWORD: mysecretpassword
     - 8000
     - db
   build: .
   image: myapp
   command: npm run serve:sw
    - 4200
   image: myapp
   command: dockerize -wait tcp://app-serve:4200 
        -wait tcp://backend:8000 
        -wait tcp://selenium-chrome-standalone:4444 -timeout 10s 
        -wait-retry-interval 1s bash -c "npm run e2e:docker"
     - app-serve
     - selenium-chrome-standalone
   image: selenium/standalone-chrome
    - 44444

In this example we added a postgres database and a container for the backend to run. Dockerize synchronizes the containers' commands.

If your system has more than one backend component add as many containers as you need. You need to wire the container dependencies properly. This means proper hostnames as environment variables on your components. And order of startup if some components depend on others.

The Selenium tests you have written should not need any modifications. You might need to put test data in the database. To keep this step in the testing area we added the seeding script before the backend startup script. This way we are sure that things happen in the proper order:

  • The DB starts and is ready to accept connections
  • A script seeds the DB data
  • The backend and the frontend start – so the tests can start


If you look at the backend container you can see there is a catch. It uses an image called mybackend built from a file located at backend/Dockerfile. This implies that your backend is in the same git repository in a folder called backend. The name is just an example of course.

If your backend and frontend are in the same repository you are good. Define the Dockerfile to build your backend and adjust the startup command to what you need.

That’s all good but usually the backend is not in the same repository. Or you can have many backend components in different repositories. What do you do then?

Multiple repositories

The super clean solution is to have a CI process on each backend component repository.

The CI process for each component runs automated tests. Upon success it pushes a docker image with the component to a private Docker registry. The backend container in our docker-compose.yml file above would use this image.

This solution requires a private Docker registry to store your images. You can use Docker Hub but then it becomes public. If you don't have one already and don’t plan to do so, it is not worth the effort.

The other solution is to use the submodules feature in git. Your backend repository becomes a virtual child of your frontend repository. You just need to add the file .gitmodules like this to your frontend repository:

[submodule "backend"]
  path = backend
  url = [email protected]:backend/repository.git
  branch = develop

Run the command git submodule update --remote whichwill pull the specified branch of the backend repository into a folder called "backend". Add as many submodules as you need if you have more than one backend component.

That’s it. Have your CI run the submodule command and from a file system perspective you are as in a monorepository.

If you don’t want the backend code locally while developing the frontend just don’t run the command. You’ll have an empty backend folder.

Versioning and backend/frontend incompatibilities

The 2 techniques above test the frontend with the latest “CI tests passed” version of your backend. That may lead to broken builds if your components are not compatible at times.

If they are compatible more often than not, stick to the “always test with the latest versions” approach. You’ll fix the occasional incompatibilities on the fly.

That won’t work, though, if incompatibilities are business as usual. In this case you need to manually control version updates. That is very easy to do.

You can lock the version of a component in the docker-compose.yml file or in the .gitmodules file. When pushing to the Docker registry you would tag the component image with the commit number of the corresponding code. The relevant docker-compose.yml file section becomes:

  image: backendapp:34028fc

Similarly the .gitmodules file would not target a branch head but a given commit:

[submodule "backend"]
  path = backend
  url = [email protected]:backend/repository.git
  branch = 34028fc

Bonus: version updates are versioned with your code. You can track which version was used for each build. This is useful when fixing failed builds or trying to reproduce old bugs.

We could push the approach to the next level. You could have a dedicated repository that would wire all your components as git modules. Bumping the versions could be a form of delivery and handover to the test/QA team.

In theory it is best to keep the latest versions of components working together more often than not. And drop the need for manual versioning. If that is not the case that is OK. Ignore the purists who will tell you that you are not following best practices and so on.

If you are just starting don't aim for the stars at first. Pick what works best for you to enjoy the benefits of automated testing right now. Then keep improving your process along the way.

Bonus on writing maintainable Selenium tests

Back to Selenium and 3 important bits of advice to help you write good UI tests.

First, avoid CSS selectors if you can. Selenium works on the DOM and can identify elements by IDs or CSS or XPath. Use IDs as much as possible even if you have to add them to your app code for only this purpose. CSS and XPath selectors are shaky. As soon as your application structure changes, they will be broken.

Second, use the Page Objects approach. It is about encapsulating your application so selectors are not directly used in tests. If your page HTML/CSS changes, your tests will have to be rewritten to use new selectors. Page Objects abstract selectors and turn them into user actions. Here is a great article on how to use Page Objects properly.

Third, don’t build long user journeys in your tests. If your tests fail at the 50th action it’s going to be difficult to reproduce and fix. Create test suites that play part of the scenarios starting from the login page. This way you are always a few clicks away from the bug your tests will catch.

Also don’t risk writing tests that rely on state from previous actions. Test suite coupling is something you want to avoid.

Let's take a practical example. Say you are testing a SaaS application for schools. The use cases could be:

  • Create a class
  • Register kids' and parents' data
  • Setup the weekly plan for the class
  • Check presence/absences
  • Input grades

Along the way you will have the login process and some navigation checks.

You could write a test that goes through the whole chain as described above. And this would be convenient because to declare kids you need a class to exist. To check presence/absences you need a weekly plan in place. And you need kids to input grades. It’s a quick win to build 1 test suite that does all these things at first.

If you have nothing at the moment and want to achieve good test coverage quickly: go for it! Done is better than perfect if it allows you to catch errors in your application now.

The cleaner solution would be to use a baseline scenario to start smaller test suites. In the example above the baseline scenario should be to create a class and register kids data.

Create a test suite that does exactly that: create a class and registered kids and parents data. Always run it first. If this stops working then you don’t need to move further on. This version of the code will never reach end users anyway.

Then create a function that encapsulates the baseline scenario. It will be duplicate code to some extent with the previous test suite. But it will allow you to have a one line function to use as a setup hook for all the other test suites. This is the best of both worlds: test scenarios starting from a fresh state in the application with minimal effort.


I hope this gave you a good insight on how you can quickly set up end to end tests for a complex system. Multiple components in multiple repositories should not be a barrier. Docker compose makes it easy to put things together.

End to end tests are the best way to avoid crunch times. In complex systems late deliveries of some components put a burden on other teams. Integrations are done in a rush. Code quality drops. That's a vicious circle. Testing often and catching cross component errors early is the solution.

Selenium tests can be done quick and dirty to get going fast. That is perfectly OK. Automate things. Then improve. Remember:

Done is better than perfect any day of the year.

Thanks for reading!

Selenium Tutorial For Beginners - Selenium Automation Testing Tutorial

Selenium Tutorial For Beginners - Selenium Automation Testing Tutorial

Selenium Tutorial For Beginners | What Is Selenium? | Selenium Automation Testing Tutorial will give you an introduction to software testing. In this Selenium tutorial, you will also get to learn the different suites of Selenium and what are the features and shortcomings of Selenium as an automation testing tool. How automation testing beats manual testing? Selenium as an automation testing tool. Selenium vs. QTP vs. IBM RFT. Advantages & Disadvantages of Selenium. Selenium suite of tools. Testing a dynamic web application

This Edureka Selenium tutorial video will give you an introduction to software testing. It talks about the drawbacks of manual testing and reasons why automation testing is the way forward. In this Selenium tutorial, you will also get to learn the different suites of Selenium and what are the features and shortcomings of Selenium as an automation testing tool. Watch the video till the very end to witness a demonstration which shows the power of Selenium as an automation testing tool.

Go through the slides in Slideshare:

Click on the time-stamp below to move directly to the topic you are interested in.
02:20 Challenges with manual testing
03:32 How automation testing beats manual testing?
13:49 Selenium as an automation testing tool
20:34 Advantages & Disadvantages of Selenium
29:48 Selenium vs. QTP vs. IBM RFT
33:41 Selenium suite of tools
37:35 Hands-on: Testing a dynamic web application

Getting started with Selenium Automation Testing

Getting started with Selenium Automation Testing

Selenium is an open source tool which is used for automating the tests carried out on web browsers (Web applications are tested using any web browser). Take a look at how you can get going with the most popular automation testing platform

Selenium is an open source tool which is used for automating the tests carried out on web browsers (Web applications are tested using any web browser). Take a look at how you can get going with the most popular automation testing platform

Selenium has become very popular among testers because of the various advantages it offers. When we talk about automation testing, the first thing that often comes to our mind is our favorite automation testing tool. Selenium won the hearts of many testers and developers with its simplicity, availability, and ease of use. With its advent in 2004, Selenium made the life of automation testers easier and is now a favorite tool for many automation testers.

What is Selenium?

Selenium was invented with the introduction of a basic tool named as “JavaScriptTestRunner,” by Jason Huggins at ThoughtWorks to test their internal Time and Expenses application. Now it has gained popularity among software testers and developers as an open source portable automation testing framework. It has the capability to automate browsers with specific browser bindings for automating web applications for testing purposes. It is a suite of four tools designed for different purposes. Let’s get to know Selenium in detail and the different tools that it offers.

Selenium Suite of Tools

Selenium has four major components with a different approach for automation testing which is popular as the Selenium suite of tools. Every software tester or developer choose tools out of it depending upon the testing requirement for the organization.

Selenium RC (Remote Control)

Selenium Core was the first tool in the suite of tools. However, it was deprecated as it had some issues related to cross-domain testing because of same origin policy. So, to overcome that, Selenium Remote Control (Selenium RC) was introduced after Selenium Core. RC turned out to be a solution to the cross-domain issue. RC has an HTTP proxy server which helps in tricking the browser into believing that both the Selenium Wore and web app which is being tested are from the same domain, removing the cross-domain issue.

Selenium RC is divided into two parts which help in overcoming the cross-domain issue:

  1. Selenium Remote Server
  2. Selenium Remote Client

But the major issue with RC was the time taken to execute a test. As the Selenium server communicates using HTTP requests, it was more time-consuming. Because of this limitation, RC also is now largely obsolete.

Selenium IDE

Selenium IDE, earlier known as Selenium recorder, is a tool used to record, edit, debug, and replay functional tests. Selenium IDE is implemented as an extension to the Chrome browser and an add-on in Firefox browser. With the Selenium IDE plugin, you can record and export tests in any of the supported programming languages like Ruby, Java, PHP, Javascript, and more.

Selenium Grid

Selenium Grid is based on a hub-node architecture. With Selenium Grid, you can run parallel test sessions across different browsers. The hub controls Selenium scripts running on different nodes (specific browsers inside an OS) and test scripts running on different nodes can be written in any programming language.

Selenium Grid was used with RC to test multiple tests on remote machines. Now, as people find **WebDriver **works better than RC, Grid works with both WebDriver and RC.

Selenium WebDriver

Selenium WebDriver is an enhanced version of Selenium RC and the most used tool. It accepts commands via the client API and sends them to browsers. Simply put, Selenium WebDriver is a browser-specific driver which helps in accessing and launching the different browsers. It provides an interface to write and run automation scripts. Every browser has different drivers to run tests.

  • Mozilla Firefox uses Firefox Driver (Gecko Driver)
  • Google Chrome uses Chrome Driver
  • Internet Explorer uses Internet Explorer Driver
  • Opera uses Opera Driver
  • Safari uses Safari Driver and
  • HTM Unit Driver used to simulate browsers using headless browser HtmlUnit

Selenium Client API

The Client API is the latest tool in the Suite of tools. With Selenium Client API, you can write test scripts in various programming languages instead of writing test scripts in Selenese. The Selenium Client API is available for Java, JavaScript, C#, Ruby, and Python. These scripts can communicate with Selenium with predefined commands and functions of Client API.

Why Use Selenium for Automation Testing?

Since we are now familiar with Selenium and its suite of tools, let’s find out the various benefits of Selenium which make it stand from the crowd as a tool for automation testing:

  1. Open-Source: Since it is an open source tool, it doesn’t require any licensing costs, which give it an upper hand over other automation testing tools.
  2. Tool for Every Need: As mentioned earlier, Selenium has a suite of tools, so it suits every need of the users. You can use various tools like WebDriver, Grid, and IDE for fulfilling your different needs.
  3. Supports All Major Languages: The major challenge that a tester or developer faces with an automation testing tool is the support for languages. Since Selenium supports all major languages like Java, JavaScript, Python, Ruby, C#, Perl, .Net and PHP, it is easier for testers to use.
  4. Browser and Operating System Support: Selenium supports different browsers like Chrome, Firefox, Opera, Internet Explorer, Edge, and Safari and different operating systems like Windows, Linux, and Mac. This makes it flexible to use.
  5. Community Support: Selenium has an active open community which helps you solve your issues and queries related to it. This makes it the best choice as your automation testing tool.

Here’s a quick comparison table of Selenium with other available tools:

Since **Selenium WebDriver **is the most used tool, we’ll be using it to execute some test cases. To understand the complete process on a very simple level, Selenium **WebDriver Architecture **consists of:

Basically, Selenium WebDriver works in three layers: Browser Driver, Remote Driver, and Language Bindings.

Core Components of WebDriver Architecture

Selenium Client Library/Language Bindings

Selenium bindings/client libraries are created by developers to support multiple programming languages. For instance, if you want to use the browser driver in Python, use the Python bindings. You can download all the bindings on the official website.

JSON Protocol Over HTTP

JavaScript Object Notation is used as a data transfer protocol to send data from a server to a client on the web. With JSON, it is very easy to write and read data with data structures like Array and Object support. This wire protocol provides a transport mechanism and defines a RESTful web service using JSON over HTTP.

Browser-Specific Driver

Each web browser has a specific browser driver for Selenium bindings. The browser driver accepts commands from the server and sends it to the browser without loss of any internal logic of browser functionalities. Browser drivers are also specific to programming languages like Ruby, C#, Java, and more for web automation.

Here are the steps when we run any test script using WebDriver:

  1. An HTTP request gets generated for every Selenium command and gets sent to browser driver.
  2. The specific browser driver receives the HTTP request through the HTTP server.
  3. HTTP Server sends all the steps to perform a function, which are executed on the browser.
  4. The test execution report is sent back to server and HTTP server sends it to the Automation script.


**Selenium WebDriver **supports all the major browser like Google Chrome, Mozilla Firefox, Internet Explorer, and Safari browsers.

Setting Up Selenium on Your Local Machine

Let’s understand the steps of how we can configure Selenium in your local machine and running a test in your local browser.

  1. Install Code editor or IDE (like Eclipse or IntelliJ)

Note: We’ll be using IntelliJ code editor for writing Automation script.

  1. Download and install Java Runtime environment in your local system.
  2. Download Java Development Kit
  3. Download and install all Java Selenium Files (Selenium Server Standalone)
  4. Install Browser Specific Drivers ( In this blog, we’ll perform Automation on Chrome, so Chrome Driver for this case)
Sample Selenium Script for Web Automation

Here is the sample automation script which can be run to automate the testing process on the local chrome browser. Since we are using IntelliJ as our code editor, so we’ll write the same in IntelliJ.

Sample Script

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
public class TestSelenium {
public static void main(String[] args){
System.setProperty("","C:\\Users\\Admin\\Desktop\\LT Automation\\chromedriver_win32\\chromedriver.exe");
WebDriver driver= new ChromeDriver();
try {
WebElement signup = driver.findElement(By.xpath("//*[@id="navbarCollapse"]/ul/li[2]/a"));;
WebElement login= driver.findElement(By.xpath("//*[@id="modalSignUp"]/div/div/div/div/div[4]/p/a"));;
String windowHandle = driver.getWindowHandle();
WebElement TextBox = driver.findElement(By.xpath("//*[@id="login-modal-form"]/div[1]/div/input"));
TextBox.sendKeys("[email protected]");
WebElement Password = driver.findElement(By.xpath("//*[@id="login-modal-form"]/div[2]/div/input"));
WebElement proceed = driver.findElement(By.xpath("//*[@id="login-modal-form"]/div[4]/button"));;
catch (Exception e) {

This code will launch a website (here,, find “Signup/Login” element, click on the Signup/login button, then go to the login page by finding “Login.” After that, enter the credentials to the login page and click the login button to be redirected to homepage.

Online Selenium Grid

The major challenge in running Selenium on a local machine is the limited number of browsers in the local machine. Since you can have only one version of a particular browser installed in your local machine, if the need comes to test on some downgraded or upgraded version of that browser, you’ll need to upgrade or downgrade the already installed browser in your local machine. Also, you can install only a specific number of browsers in the system. So, if the need comes it becomes almost impossible to test across all browsers and operating systems. That's where an online Selenium Grid can help.

With the help of an online Selenium Grid on the cloud, you can test across all the browsers, browser versions, operating systems, resolutions for cross-browser compatibility. Online platforms which provide Selenium Grids, like LambdaTest, SauceLabs, and BrowserStack, can help you perform cross-browser tests on cloud grid of various browsers-OS combinations.

Common Selenium Command and Operations

While writing an automation script, you will be using many repeated commands and doing various operations. Let’s have a quick look at the most common and used commands in Selenium automation testing.

**Page Visit: **The first thing to do visit a webpage to start automation testing.


**Find an Element: **Find elements to automate them.

// find just one, the first one Selenium finds
WebElement element = driver.findElement(locator);
// find all instances of the element on the page
List<WebElement> elements = driver.findElements(locator);

**Actions on Elements: **Work on found elements.

// chain actions together
// store the element and then click it

WebElement element = driver.findElement(locator);;

**Multiple Element Commands: **Common commands to click, submit, clear, input, etc.; // clicks an element
element.submit(); // submits a form
element.clear(); // clears an input field of its text
element.sendKeys("input text"); // types text into an input field

**Question Commands: **Check conditions for elements.

element.isDisplayed(); // is it visible to the human eye?
element.isEnabled(); // can it be selected?
element.isSelected(); // is it selected?

**Get your Info: **Commands for retrieving information for an element.

// directly from an element
// by attribute name

To Sum Up

Selenium is one of the best automation testing tools to automate web browser interactions. You can perform automation testing by writing code in any of your preferred language supported by Selenium and can easily run your automation script to automate testing of an application or a process. Its ease of use makes it different from other tools and with the help of an online grid you can even run your tests in parallel across more than one browser. So, what are you waiting for? Write a beautiful automation Script and test your website! If you have any questions, let us know in the comments section below.

Happy Testing!

Test Automation Using Pytest and Selenium WebDriver

Test Automation Using Pytest and Selenium WebDriver

Test Automation Using Pytest and Selenium WebDriver: For all your cross-browser, multi-device testing needs, look no further than the powerful combination of Selenium WebDriver and pytest.

Test Automation Using Pytest and Selenium WebDriver: For all your cross-browser, multi-device testing needs, look no further than the powerful combination of Selenium WebDriver and pytest.

One of the challenges that developers face is ensuring that their web application works seamlessly across a different set of devices, browsers, and operating systems platforms. This is where cross-browser testing plays a very crucial role in testing the web application since it helps in testing across different combinations. Based on the target market, development and product teams need to chart out a plan for the various activities involved in cross-browser compatibility testing.

Selenium – Introduction and WebDriver Interface

As far as testing a web application is concerned, there a couple of web frameworks available that automate the tests performed across different web browsers. Selenium is a very popular framework that is primarily used for the automation testing of web applications. It is an open-source tool with which web testing can be performed against popular browsers like Chrome, Firefox, Opera, and Microsoft Edge. The framework can also be used if the test has to be performed on Internet Explorer (latest version or the legacy versions).

Selenium WebDriver is considered one of the core components of the Selenium framework. Selenium WebDriver API is a collection of open-source APIs/language-specific bindings that accepts commands and sends them to the browser, against which the testing is performed. The individual who is responsible for developing the tests need not bother about the architecture details or other technical specifications of the web browser since WebDriver acts as an interface between the test suite/test case and web browser (achieved using the browser-specific WebDriver).

Selenium WebDriver supports different programming languages like Python, C#, Ruby, PERL, and Java. The diagram below shows a simplified view of the Selenium WebDriver Interface. We have already covered the Selenium WebDriver architecture in-depth in our earlier post.

Pytest Test Framework – Introduction and Advantages

Python has a couple of test frameworks that ease the task of web application testing; unittest and pytest are the most widely used frameworks. unittest is a part of the standard library (in Python) and comes as a part of the Python installation. For test automation using pytest, the more popular of the two, with Selenium WebDriver, you need to install pytest separately. Here are some of the advantages of the pytest framework:

  • Can be used by development teams, test teams, teams that are practicing Test-Driven Development (TDD), as well as in open-source projects.
  • Can be used in simple, as well as complex, functional test cases for applications and libraries.
  • Easy to port existing test suites to pytest for performing test automation using pytest with Selenium WebDriver.
  • Compatibility with other test frameworks like unittest and nose, so switching to this framework is very easy.
  • Supports parameterizing, which is instrumental in executing the same tests with different configurations using a simple marker. You can come up with more effective test cases/test suites with less repetitive code implementation.
  • The highh number of asserts that provides more detailed information about the failure scenarios.
  • Support of Fixtures and Classes. Using Fixtures, it becomes easy to make common test objects available throughout a module, session, function, or class. Fixtures and Classes will be covered in more detail in subsequent sections.
  • Good and up-to-date documentation.
  • xdist support through which test cases can be parallelized.

To summarize, Pytest is a software test framework which can be used to make simple, yet scalable test cases with ease.

Now that you are aware of the advantages of pytest over other test frameworks, let’s have a detailed look at the pytest framework and how it can be used with Selenium WebDriver framework in order to perform automated cross-browser testing for web applications.

Test Automation Using Pytest – Installation and Getting Started

As mentioned earlier, pytest is not a part of the standard Python installation and needs to be installed separately. In order to install pytest, you should execute the following command on the prompt/terminal:

pip install –U pytest

Once the installation is complete, you can verify whether the installation is successful, by typing the following command:

pytest --version

Below is the output when the above command is executed on Linux and Windows machine

PyCharm is a popular IDE that is used for pytest development. You can install the PyCharm Edu version for Windows, Linux, or macOS. For development, we are using PyCharm for Windows. Once PyCharm is installed, you should make sure that the default test runner is pytest. In order to change the default test runner, you should navigate to File -> Settings -> Tools -> Python Integrated Tools and change Default test runner for performing test automation using pytest with Selenium WebDriver.

Now that PyCharm Edu is installed and the default test runner is set to pytest, you need to install the Selenium package for Python to perform test automation using pytest with Selenium WebDriver. In order to install Selenium, you should invoke the command mentioned below in the terminal of PyCharm.

pip install -U selenium ( Syntax – pip install –U )

Shown below is the snapshot of the command execution:

Now that your development environment is all set, we look into some of the features and aspects of pytest.

Pytest – Usage, Exit Codes, and Compilation

pytest and py.test can be used interchangeably. In order to get information about the arguments that can be used with pytest, you can execute the command below on the terminal.

pytest --help     
#Command to get help about the options that can be used with pytest command 
# Details about fixtures pytest --fixtures  #Shows the available built-in function arguments

When pytest code is executed, it results in one of the following exit codes:


| 0 | Test cases/test suites are executed successfully and end result was PASS |

| 1 | Test cases/test suites were executed, but some tests FAILED |

| 2 | Test execution was stopped by the user |

| 3 | Unknown error occurred when the tests were executed |

| 4 | Usage of pytest command is incorrect |

| 5 | No tests were collected |

It is important that the file containing pytest code be named as ** or In order to compile and execute pytest source code for performing test automation using pytest with Selenium WebDriver, you can use the following command on the terminal

pytest <> --verbose --capture=no

Let’s have a look at some examples of test automation using pytest. We start with a very simple example –

#pytest in action – 
def function_1(var):   
return var + 1   
def test_success():   
assert function_1(4) == 5   
def test_failure():   
assert function_1(2) == 5

In the above code snippet, we create a function named function_1  which takes one argument named var . There are two test cases:  test_success()  and  test_failure() . The test cases are executed in serial order and the assert is issued on an execution of the test cases. Compile the code using the command mentioned below

pytest --verbose --capture=no

As seen in the output, the result of the first test case is PASS (shown in blue) and a result of the second test case is FAIL (shown in red).

pytest makes use of the assert available in Python for verification of results. It gives out meaningful information which can be used for verification and debugging. pytest.raises is commonly used to raise exceptions; below is an example where a Factorial of a number is calculated. In one test case, a negative number is passed as an input to the factorial function and AssertionError is raised.  contains the implementation that uses recursion in order to calculate factorial of the input number. Before the factorial is calculated, the input parameter check is performed. Assert would be raised in case the input number is negative.

def factorial_function(number):    
# Perform a check whether the input number is positive or not, if it is not    
# positive, raise an assert     
assert number >= 0\. and type(number) is int, "The input is not recognized"       
if number == 0:         
return 1     
# recursive function to calculate factorial         
return number * factorial_function(number – 1)  is a pytest implementation which use factorial functionality. Three test cases are implemented –  test_standard_library  (output from factorial_function is compared with the output obtained from math.factorial module),  test_negative_number  (assertion is raised when the input number is negative), and (results of output from factorial_function are compared with specific values).

# Import the necessary modules/packages required for implementation 
import pytest import math   
from factorial_example import factorial_function  
def test_factorial_functionality():     
print("Inside test_factorial_functionality")       
assert factorial_function(0) == 1     
assert factorial_function(4)== 24   
def test_standard_library():     
print("Inside test_standard_library")      
for i in range(5): 
# verify whether factorial is calculated correctly       
# by checking against result against  standard       
# library - math.factorial()         
assert math.factorial(i) == factorial_function(i)   
def test_negative_number():     
print("Inside test_negative_number")      
# This test case would pass if Assertion Error    
# is raised. In this case, the input number is negative    
# hence, the test case passes     
with pytest.raises(AssertionError):         

You can execute the code using the command py.test –capture=no , either on the command prompt or on the Terminal of PyCharm IDE. As seen in the snapshot, all the test cases have passed and logs under “print statement” are output on the console

Test Automation Using Pytest – Fixtures (Usage and Implementation)

Consider an example where you have to execute certain MySQL queries on a database that contains employee information within an organization. The time taken to execute a query would depend on the number of records (i.e. employees) in the database. Before queries are executed, required operations (w.r.t database connectivity) have to be performed and the “returned handle” would be used in a subsequent implementation involving the database. Database operations can be CPU intensive (as the number of records increases); hence, repetitive implementation and execution should be avoided. There are two ways in which this issue can be solved:

  1. With the help of classic xunit style setup along with teardown methods.
  2. By using fixtures (recommended).

The xunit style of fixtures is already supported in unittest but pytest has a much better way of dealing with fixtures. Fixtures are a set of resources that have to set up before the test starts and have to be cleaned up after the execution of tests is complete. It contains a lot of improvements over the classic implementation of setup and teardown functions. The main advantages of using fixtures are

  • Can be used by development teams, test teams, teams that are practicing Test-Driven Development (TDD), as well as in open-source projects.
  • Can be used in simple, as well as complex, functional test cases for applications and libraries.
  • Easy to port existing test suites to pytest for performing test automation using pytest with Selenium WebDriver.
  • Compatibility with other test frameworks like unittest and nose, so switching to this framework is very easy.
  • Supports parameterizing, which is instrumental in executing the same tests with different configurations using a simple marker. You can come up with more effective test cases/test suites with less repetitive code implementation.
  • The highh number of asserts that provides more detailed information about the failure scenarios.
  • Support of Fixtures and Classes. Using Fixtures, it becomes easy to make common test objects available throughout a module, session, function, or class. Fixtures and Classes will be covered in more detail in subsequent sections.
  • Good and up-to-date documentation.
  • xdist support through which test cases can be parallelized.

Ever since the launch of version 3.5, the fixtures of higher scope are prioritized above the lower scope fixtures in terms of instantiating. Higher scope fixture includes sessions, and lower scope fixture would include classes, functions, and others. You can even ‘‘parameterize" these fixture functions in order to execute them multiple times along with the execution of dependent tests.

Fixture parameterization has been widely used to write exhaustive test functions. Below is a simple code for test automation using pytest where setup() and teardown()  of ‘resource 1’ is called, even when the test_2 is executed. Since this is a simple implementation (with fewer computations), there are not many overheads even when unnecessary setup and module calls are invoked, but it could hamper the overall code performance in case any CPU-intensive operations (like database connectivity) are involved.

#Import all the necessary modules import pytest   
def resource_1_setup():     
print('Setup for resource 1 called')   
def resource_1_teardown():     
print('Teardown for resource 1 called')   
def setup_module(module):     
print('\nSetup of module is called')     
def teardown_module(module):     
print('\nTeardown of module is called')     
def test_1_using_resource_1():     
print('Test 1 that uses Resource 1')   
def test_2_not_using_resource_1():     
print('\nTest 2 does not need Resource 1')

Execute the test case ‘test_2_not_using_resource_1’ by invoking the following command on the terminal:

pytest --capture=no --verbose

As observed from the output [Filename – Pytest-Fixtures-problem.png], even though “test_2” is executed, the fixture functions for “resource 1” are unnecessarily invoked. This problem can be fixed by using fixtures; we will have a look at these in the upcoming example.

As seen in the example below, we define a fixture function resource_1_setup()  (similar to setup in xunit style implementation) and resource_1_teardown()  (similar to teardown in xunit style implementation). The fixture function has “module scope” using @pytest.fixture(scope=’module’) .

#Import all the necessary modules import pytest   
#Implement the fixture that has module scope 
def resource_1_setup(request):     
print('\nSetup for resource 1 called')       
def resource_1_teardown():         
print('\nTeardown for resource 1 called')       
# An alternative option for executing teardown code is to make use of the addfinalizer method of the request-context     
# object to register finalization functions.     
# Source -     
def test_1_using_resource_1(resource_1_setup):     
print('Test 1 uses resource 1')   
def test_2_not_using_resource_1():     
print('\n Test 2 does not need Resource 1')

We execute the code by triggering all the test cases. As shown in the output below [Filename – Pytest-Fixtures-all-tests-executed.png], “setup for resource 1” is called only for Test 1 and not for Test 2.

Now, we execute only test case 2, that is, test_2_not_using_resource_1() . As seen in the output below [Filename – Pytest-Fixtures-only-2-tests-executed.png], setup and teardown functions for Resource 1 are not called since the only test case 2 is executed. This is where fixtures can be highly effective since it eliminates repetitive code and execution of unnecessary code. Official documentation about fixtures in pytest can be found here.

Test Automation Using Pytest with Selenium WebDriver

When you are looking out for a test automation framework, you would probably require a test framework that meets all your requirements. The framework should have the ability to log events, generate test reports, and should have good community support. Pytest fulfils all these requirements and test automation using pytest with Selenium WebDriver is highly recommended as it does not involve a steep learning curve.

When you are planning to develop test automation using pytest with Selenium WebDriver, the first concern that you need to look into is when you should load the browser. Loading a new browser instance after each test is not recommended since it is not a scalable solution and might increase the overall test execution time. It is recommended to load the browser (under test) before the actual test cases have started and unloaded/closed the browser instance as soon as the tests are complete. This is possible by using Fixtures in pytest. As mentioned earlier, Fixtures make extensive use of a concept know as dependency injection, where dependencies can be loaded before the actual tests have started.

By default, fixtures have function scope, depending on the requirements; you can change the implemented fixture’s scope to a module, session, or class. Like the lifetime of variables in C language, the scope of fixtures indicates how many times the particular fixture will be created.


| Function | Fixture is executed/run once per test session |

| Session | One fixture is created for the entire test session |

| Class | Only one fixture is created per class of tests |

| Module | Fixture is created once per module |

Once the tests have been executed, you might be interested to capture the test results in a report format (like HTML). You need to install pytest-html module for the same

pip install pytest-html

Below is the snapshot of the command in execution:

Now that you have knowledge about pytest fixtures, Selenium, and Selenium WebDriver interface, let’s have a look at an example with all these things in action. Before you start the implementation, please ensure that you download Gecko driver for Firefox and ChromeDriver for Chrome from here and here respectively. In order to avoid mentioning the path/location where the drivers have been downloaded, make sure that you place these respective drivers at the location where the corresponding browsers are present. In the snapshot below, you can see that we have copied Geckodriver.exe in the location where Firefox browser (firefox.exe) is present.

Now that you have the setup ready, let’s get started with the implementation. Import all the necessary modules in the beginning so that you avoid errors. In our case, the modules imported are selenium, pytest, pytest-html. Two fixture functions – driver_init()  and  chrome_driver_init()  have the “class” scope. As seen in the fixture function driver_init() , an instance of Firefox is created using GeckoDriver, whereas in chrome_driver_init() , an instance of Chrome browser is created using ChromeDriver. yield contains the implementation of teardown; code inside yield is responsible for doing the cleanup activity. A class is used to group test cases, in this case, there are two important classes, Test_URL()  and Test_URL_Chrome() . The implemented classes are making use of the fixtures that were implemented using mark.usefixtures [ @pytest.mark.usefixtures(“driver_init”) ]. The test case performs a simple test of invoking the respective browser (Firefox/Chrome) and opening the supplied URL i.e. Filename –

# Import the 'modules' that are required for execution   
import pytest import pytest_html from selenium 
import webdriver from 
import Options from selenium.webdriver.common.keys 
import Keys from time import sleep   
#Fixture for Firefox @pytest.fixture(scope="class") 
def driver_init(request):     ff_driver = webdriver.Firefox()     
request.cls.driver = ff_driver     yield     ff_driver.close()   
#Fixture for Chrome @pytest.fixture(scope="class") 
def chrome_driver_init(request):     
chrome_driver = webdriver.Chrome()     
request.cls.driver = chrome_driver     yield     chrome_driver.close()   
@pytest.mark.usefixtures("driver_init") class BasicTest:     pass class Test_URL(BasicTest):         
def test_open_url(self):             
class Basic_Chrome_Test:     
pass class Test_URL_Chrome(Basic_Chrome_Test):         
def test_open_url(self):             

Since we require the test output in an HTML file, we make us of –html=&nbsp;argumentt while executing the test code. The complete command to execute test automation using pytest with Selenium WebDriver:

| 1 |

py.test.exe --capture=no --verbose --html=pytest_selenium_test_report.html


Below is the execution output, testcase test_open_url() is executed for the class Test_URL  and Test_URL_Chrome() . The test report is pytest_selenium_test_report.html [Image – PyTest-Selenium-Output-1.png]. Here is a test report for further clarity.

As seen in the above implementation, the only difference between fixture function for Firefox and Chrome browser is the setting up of the respective browser. The majority of the implementation is same for both the browsers, so it becomes important to optimize the code by avoiding repetition of code. This is possible by making use of parameterized fixtures. As seen in the implementation [Filename –], the major change is addition of parameters to fixtures, as in @pytest.fixture(params=[“chrome”, “firefox”],scope=”class”) . Depending on the browser in use, the corresponding WebDriver is used to invoke the browser.

# Import the 'modules' that are required for execution   
import pytest import pytest_html from selenium 
import webdriver from 
import Options from selenium.webdriver.common.keys 
import Keys from time import sleep   
#Fixture for Firefox @pytest.fixture(params=["chrome", "firefox"],scope="class") 
def driver_init(request):     
if request.param == "chrome":         
web_driver = webdriver.Chrome()     
if request.param == "firefox":         
web_driver = webdriver.Firefox()     
request.cls.driver = web_driver     yield     
@pytest.mark.usefixtures("driver_init") class BasicTest:     
pass class Test_URL(BasicTest):         
def test_open_url(self):             

In our case, we are using the Chrome and Firefox browsers and the test case Test_URL()  would be executed for each browser separately. As seen in the output, the test case is invoked once with parameters as “firefox” and “chrome.”

Cross-Browser Testing With Pytest, Selenium and Lambdatest

There is always a limitation on the amount of testing that you can perform on your local machine or test machines since thorough testing has to be performed on different kinds of devices, operating systems, and browsers. Setting up a local test environment is not a scalable and economical option. This is where your test team can utilize the power of Lambdatest’s cross-browser testing on the cloud capabilities.

You can perform manual as well as automated cross-browser testing of your web application or website on different browsers (even old versions) and devices. You can also perform real-time testing by using their Tunnel feature which lets you use their test infrastructure from the terminal. LambdaTest Selenium Automation Grid enables you to perform end-to-end automation tests on a secure, reliable, and scalable Selenium infrastructure. You can utilize the LambdaTest Selenium Grid to not only increase the overall code-coverage (via testing), but to also decrease the overall time required to execute your automation scripts written in Python.


Test automation using Pytest with Selenium WebDriver is a very favourable option as a framework that has good features with which test engineers can come up with implementation that is easy to implement and which is scalable. It can be used for writing test cases for simple scenarios as well as highly complex scenarios. A developer who is well-versed with the Python, unittest/other test frameworks based on Python would find pytest easy to learn. With pytest leverages concepts like dependency injection, there is less cost involved in the maintainability of the source code.

Since the number of devices are increasing with each passing day, it becomes highly impractical to manually test your code against different devices, operating systems, and browsers; this is where testers/developers can utilize Lambdatest’s cross-browser testing tool, which allows you to perform test automation using pytest with Selenium WebDriver effortlessly.