This tutorial will make web UI testing easy. We will build a simple yet robust web UI test solution using Python, pytest, and Selenium WebDriver. We will learn strategies for good test design as well as patterns for good automation code. By the end of the tutorial, you’ll be a web test automation champ! Your Python test project can be the foundation for your own test cases, too.
- Set Your Test Automation Goals (Chapter 1)
- Create A Python Test Automation Project Using Pytest (Chapter 2)
- You’re here → Installing Selenium WebDriver Using Python and Chrome (Chapter 3)
- Write Your First Web Test Using Selenium WebDriver, Python and Chrome (Chapter 4)
- Develop Page Object Selenium Tests Using Python (Chapter 5)
- How to Read Config Files in Python Selenium Tests (Chapter 6)
- Take Your Python Test Automation To The Next Level (Chapter 7)
- Create Pytest HTML Test Reports (Chapter 7.1)
- Parallel Test Execution with Pytest (Chapter 7.2)
- Scale Your Test Automation using Selenium Grid and Remote WebDrivers (Chapter 7.3)
- Test Automation for Mobile Apps using Appium and Python (Chapter 7.4)
- Create Behavior-Driven Python Tests using Pytest-BDD (Chapter 7.5)
With our new test project in place, let’s write some web UI tests with Selenium WebDriver!
WebDriver is a programmable interface for interacting with live web browsers. It enables test automation to open a browser, send clicks, type keys, scrape text, and ultimately exit the browser cleanly. The WebDriver interface is a W3C Recommendation. The most popular implementation of the WebDriver standard is Selenium WebDriver, which is free and open source.
WebDriver has multiple components:
- Automation Code. Programmers use language bindings to automate browser interactions. Common interactions include finding elements, clicking them, and scraping text. Typically, this is written with a test automation framework.
- JSON Wire Protocol. Language bindings encode every interaction using JSON and send them as REST API requests to the browser’s driver. The JSON wire protocol is platform- and language- independent.
- Browser Driver. The driver is a standalone executable on the test machine. It acts as a proxy between the interaction’s caller and the browser itself. It receives JSON requests for interactions and sends them to the browser using HTTP.
- Browser. The browser renders the web pages under test. It is essentially controlled by the driver. All major browsers support WebDriver. Each browser also needs its own driver type installed on the same machine as the browser and accessible from the system path. For example, Google Chrome requires ChromeDriver.
For our test project, we will use Selenium WebDriver’s Python bindings with Google Chrome and ChromeDriver. We could use any browser, but let’s use Chrome because (a) it has a very high market share and (b) its Developer Tools will come in handy later.
Make sure that the most recent version of Chrome is installed on your machine (To check/update Chrome, go to the menu and select Help > About Google Chrome. Or, download and install it here.) Then, download the matching version of ChromeDriver here and add it to your system path.
Verify that ChromeDriver works from the command line:
$ chromedriver Starting ChromeDriver 73.0.3683.68 (47787ec04b6e38e22703e856e101e840b65afe72) on port 9515 Only local connections are allowed. Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.
Then, install Python’s
selenium package into our environment:
$ pipenv install selenium --dev
Now, the machine should be ready for web testing!
Create a new Python module under the
tests/ directory named
test_web.py. This new module will hold our web UI tests. Then, add the following import statements:
import pytest from selenium.webdriver import Chrome from selenium.webdriver.common.keys import Keys
Why do we need these imports?
pytestwill be used for fixtures
Chromeprovides ChromeDriver binding
Keyscontains special keystrokes for browser interactions
As a best practice, each test case should use its own WebDriver instance. Although the setup and cleanup adds a few seconds to each test, using one WebDriver instance per test keeps tests simple, safe, and independent. If one test hits a problem, then other tests won’t be affected. Plus, using a separate WebDriver instance for each test enables tests to be run in parallel.
WebDriver setup is best handled using a pytest fixture. Fixtures are pytest’s spiffy setup and cleanup functions that can also do dependency injection. Any test requiring a WebDriver instance can simply call the fixture to get it.
Add the following code to
@pytest.fixture def browser(): driver = Chrome() driver.implicitly_wait(10) yield driver driver.quit()
browser is a pytest fixture function, as denoted by the
@pytest.fixture decorator. Let’s step through each line to understand what this new fixture does.
driver = Chrome()
Chrome() initializes the ChromeDriver instance on the local machine using default options. The driver object it returns is bound to the ChromeDriver instance. All WebDriver calls will be made through it.
The most painful part of web UI test automation is waiting for the page to load/change after firing an interaction. The page needs time to render new elements. If the automation attempts to access new elements before they exist, then WebDriver will raise a
NoSuchElementException. Improper waiting is one major source of web UI test “flakiness.”
implicitly_wait method above tells the driver to wait up to 10 seconds for elements to exist whenever attempting to find them. The waiting mechanism is smart: instead of sleeping for a hard 10 seconds, it will stop waiting as soon as the element appears. Implicit waits are declared once and then automatically used for all elements. Explicit waits, on the other hand, can provide custom waiting for each interaction at the cost of requiring explicit waiting calls. As a best practice, use one style of waiting exclusively for test automation. Mixing explicit and implicit waits can have nasty, unexpected side effects. For our test project, an implicit wait of 10 seconds should be reasonable (If your Internet connection is slow, please increase this timeout to compensate).
A pytest fixture should return a value representing whatever was set up. Our fixture returns a reference to the initialized WebDriver. However, instead of using a
return statement, it uses
yield, meaning that the fixture is a generator. The first iteration of the fixture – in our case, the WebDriver initialization – is the “setup” phase to be called before a test begins. The second iteration – which will be the
quit call – is the “cleanup” phase to be called after a test completes. Writing fixtures as generators keeps related setup and cleanup operations together as one concern.
Always quit the WebDriver instance at the end of a test, no matter what happens. Driver processes on the test machine won’t always die when test automation ends. Failing to explicitly quit a driver instance could leave it running as a zombie process, which could consume and even lock system resources.
Now that we have the WebDriver ready to go, we can write our first web UI test! Check it out here 😎