Published on

Automated UI Testing with Selenium & PyTest

Authors

Intro

User Interface (UI) Testing is a key component of the software development life-cycle that helps to ensure the shipping of high quality software. However, a common problem with UI Testing is that it is slow & tedious when done manually - especially when working with large, complex, modern web applications. Thus, many software teams are turning to automation in order to more quickly and efficiently execute UI Testing.

This article outlines the process of setting up an Automated UI Testing suite that can be adapted / extended to almost any web application. The tools being used are:

  • Python3 - one of the most popular programming languages with a huge community and countless resources available online
  • PyTest - a framework that makes building simple and scalable tests easy
  • Selenium - a toolset for web browser automation that uses the best techniques available to remotely control browser instances and emulate a user’s interaction with the browser

Development Environment

Prerequisites

You must have Python v3.7+ installed on your machine along with pipenv. You'll also want to have an IDE - a common choice is PyCharm

Getting Started

Create a new project directory & activate a virtual environment

$ mkdir pytest_demo && cd pytest_demo && pipenv shell

Install dependencies

$ pipenv install selenium pytest pytest-xdist python-dotenv

WebDrivers

According to the MDN Web Docs, a WebDriver is:

a remote control interface that enables introspection and control of user agents. It provides a platform- and language-neutral wire protocol as a way for out-of-process programs to remotely instruct the behavior of web browsers.

A WebDriver will be required for each browser you're using to execute tests on (typically: Chrome, Firefox, Safari). Download a copy via these links:

Note that the ChromeDriver versions needs to match the Chrome browser version being used (when you update Chrome, you'll need to download the new, corresponding driver again). Once downloaded, move the files to your /usr/local/bin directory - Selenium knows to look for them here.

$ sudo unzip chromedriver_linux64.zip -d /usr/local/bin
$ sudo tar -xf geckodriver-v0.29.0-linux64.tar.gz -C /usr/local/bin

Check that the driver were installed properly (use Ctrl + c to stop the processes):

$ chromedriver --version
ChromeDriver 89.0.4389.23 (61b08ee2c50024bab004e48d2b1b083cdbdac579-refs/branch-heads/4389@{#294})

$ chromedriver
Starting ChromeDriver 89.0.4389.23 (61b08ee2c50024bab004e48d2b1b083cdbdac579-refs/branch-heads/4389@{#294}) on port 9515
Only local connections are allowed.
Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
ChromeDriver was started successfully.

$ geckodriver --version
geckodriver 0.29.0 (cf6956a5ec8e 2021-01-14 10:31 +0200)

The source code of this program is available from
testing/geckodriver in https://hg.mozilla.org/mozilla-central.

This program is subject to the terms of the Mozilla Public License 2.0.
You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.

$ geckodriver
1617662745505   geckodriver INFO    Listening on 127.0.0.1:4444

You can place the drivers anywhere but will need to specify the filepath in your webdriver initialization method later on.

Test Development

The project will be divided up into pages and tests and contain a config.json file at the project root. Setup your project to look like:

.
├── config.json
├── pages
│   ├── __init__.py
├── Pipfile
├── Pipfile.lock
└── tests

Configuration Details

We'll want to specify a few key items that the tests will need to know in order to run - namely, which browser to use and what URL to navigate to once the window is launched. In the config.json file add:

{
  "browser": "Chrome",
  "implicit_wait": 10,
  "target_url": "http://automationpractice.com/"
}

Also, at this point, you'll want to create an account on this site to use for testing. Navigate to Automated Practice's create an account page and manually create a new test account. Then add the email address, password, and user name to a .env in your project root.

Webdriver Initialization

Each test will need an instance of the webdriver so let's set that up first. In your tests directory, add a conftest.py file. PyTest know that when starting any test, to use the conftext.py file to setup test configuration details. Within this file add:

"""
This module contains shared fixtures
"""

import json
import pytest
from selenium import webdriver


@pytest.fixture
def config(scope="session"):

    # read config file
    with open('config.json') as config_file:
        config = json.load(config_file)

    # assert values are acceptable
    assert config['browser'] in ['Firefox', 'Chrome', 'Headless Chrome']
    assert isinstance(config['implicit_wait'], int)
    assert config['implicit_wait'] > 0

    return config


@pytest.fixture()
def browser(config):
    if config['browser'] == 'Firefox':
        wd = webdriver.Firefox()
    elif config['browser'] == 'Chrome':
        wd = webdriver.Chrome()
    elif config['browser'] == 'Headless Chrome':
        opts = webdriver.ChromeOptions()
        opts.add_argument('headless')
        wd = webdriver.Chrome(options=opts)
    else:
        raise Exception(f'The "{config["browser"]}" browser is not supported')
    # set calls to wait for up to 10 seconds for elements
    wd.implicitly_wait(config['implicit_wait'])
    # resize browser window
    wd.maximize_window()
    wd.get(config['target_url'])
    # return the WebDriver instance for the setup
    yield wd
    # Quit the WebDriver instance for the cleanup
    wd.quit()

Page Object Model

Each route / page of a web application should have its own Page Object Model - that is locators and actions specific to that page. As a example, add a nav.py file to the pages directory. This file will contain locators and methods for the navigation bar. Within this file add:

"""
This module contains HeaderNav,
the page object for the Automation Practice header navigation bar
"""

from selenium.webdriver.common.by import By


class HeaderNav:

    SIGN_IN = (By.CSS_SELECTOR, 'a[class="login"]')
    SIGN_OUT = (By.CSS_SELECTOR, 'a[class="logout"]')
    USERNAME = (By.CSS_SELECTOR, 'a[class="account"]')
    CONTACT = (By.ID, 'contact-link')

    def __init__(self, browser):
        self.browser = browser

    def navigate_to(self, page):
        if page == 'signin':
            self.browser.find_element(*self.SIGN_IN).click()
        elif page == 'contact':
            self.browser.find_element(*self.CONTACT).click()
        elif page == 'logout':
            self.browser.find_element(*self.SIGN_OUT).click()

    def verify_user_login(self, username):
        username_display = self.browser.find_element(*self.USERNAME)
        return username_display.is_displayed() and username_display.text == username

In order to execute a "User Can Successfully Login" test case, we'll need to click 'Sign In' from the Nav and then input credentials. So next let's add a Page Object Model for the auth page:

"""
This module contains AuthPage,
the page object for the Automation Practice login / signup page
"""
import os
from dotenv import load_dotenv
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys


class AuthPage:
    load_dotenv()
    EMAIL_INPUT = (By.ID, 'email')
    PASSWORD_INPUT = (By.ID, 'passwd')
    SIGNIN_BUTTON = (By.ID, 'SubmitLogin')
    USER_EMAIL = os.getenv('EMAIL')
    USER_PASSWORD = os.getenv('PASSWORD')

    def __init__(self, browser):
        self.browser = browser

    def input_login_credentials(self):
        email_input = self.browser.find_element(*self.EMAIL_INPUT)
        password_input = self.browser.find_element(*self.PASSWORD_INPUT)
        email_input.send_keys(self.USER_EMAIL + Keys.TAB)
        password_input.send_keys(self.USER_PASSWORD)

    def click_sigin_button(self):
        self.browser.find_element(*self.SIGNIN_BUTTON).click()

Tests

Now that we've got a couple of page object models (for the pages need in order to login a user), let's put together an actual test. Add a test_auth.py file to the tests directory (the test_ prefix is important, it is how pytest recognizes tests). Within this file add:

"""
These tests cover Automation Practice Login / Signup page
"""

import os
from dotenv import load_dotenv
from pages.auth import AuthPage
from pages.nav import HeaderNav


def test_UserLogin(browser):
    load_dotenv()
    username = os.getenv('USERNAME')
    nav = HeaderNav(browser)
    auth = AuthPage(browser)
    nav.navigate_to('signin')
    auth.input_login_credentials()
    auth.click_sigin_button()
    assert nav.verify_user_login(username)

Running Tests

To execute tests, run:

$ pipenv run python -m pytest

You should see the browser launch and the test steps being executed. Once the test is completed, you'll see output in console similiar to:

(pytest_demo) ➜  pytest_demo git:(main) pipenv run python -m pytest tests/test_auth.py 
Loading .env environment variables...
======================================= test session starts ========================================
platform linux -- Python 3.8.5, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/tim/Projects/Selenium/pytest_demo
plugins: xdist-2.2.1, forked-1.3.0
collected 1 item                                                                                   

tests/test_auth.py .                                                                         [100%]

======================================== 1 passed in 7.93s =========================================

Resources