Test Automation Symphony

 

E2E Web Testing with Python, Pytest and Selenium Webdriver

By Alejandro Serrano

Outline

  • Importance of Test Automation
  • Test Automation Pyramid
  • Purpose of an E2E test
  • Reasoning behind E2E tests
  • How to write E2E test cases?

Importance of Test Automation

Add Value

Test Automation Pyramid

Manual tests

Ideal Test Automation Pyramid

Ice Cream Cone Anti-pattern

Unit tests

Integration tests

UI tests

UI tests

Integration tests

Unit tests

Manual tests

Purpose of an E2E test

Mimic how people tend to use the system under test

Reasoning behind E2E tests

  • Test Ownership
  • Coverage
  • Rapid Releases

Not everything shines...

  • Stability - Flakiness
  • Execution Speed
  • Hard to read and write

Here we go...

Python

Pytest

Selenium

Design

Patterns

Page Object Model

  • Locators
  • UI Definition
  • Actions

Please don't...

def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')

    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()

Please don't...

def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored
    time.sleep(1)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()

Please don't...

def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored
    time.sleep(3)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()

Please don't...

def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored.
    time.sleep(3)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()
    
    # First click does not work sometimes.
    login_btn.click()

System Under Test (SUT)

<div class="login-box">
  <form>
    <input type="text" class="form_input" data-test="username" id="user-name" placeholder="Username" value="">
    <input type="password" class="form_input" data-test="password" id="password" placeholder="Password" value="">
    <input type="submit" class="btn_action" value="LOGIN">
  </form>
</div>

System Under Test (SUT)

<div class="login-box">
  <form>
    <input type="text" class="form_input" data-test="username" id="user-name" placeholder="Username" value="">
    <input type="password" class="form_input" data-test="password" id="password" placeholder="Password" value="">
    <input type="submit" class="btn_action" value="LOGIN">
  </form>
</div>

System Under Test (SUT)

<div class="login-box">
  <form>
    <input type="text" class="form_input" data-test="username" id="user-name" placeholder="Username" value="">
    <input type="password" class="form_input" data-test="password" id="password" placeholder="Password" value="">
    <input type="submit" class="btn_action" value="LOGIN">
  </form>
</div>

System Under Test (SUT)

<div class="login-box">
  <form>
    <input type="text" class="form_input" data-test="username" id="user-name" placeholder="Username" value="">
    <input type="password" class="form_input" data-test="password" id="password" placeholder="Password" value="">
    <input type="submit" class="btn_action" value="LOGIN">
  </form>
</div>

Development Environment

  • Add the python root and scripts folders to your PATH environment variable

demo

drivers

src

login

tests

login

  • Create the following folder structure in your system

Development Environment

demo

drivers

src

login

tests

login

  • Add the following python files to the folder structure

__init__.py

login.py

__init__.py

test_login.py

__init__.py

conftest.py

Development Environment

demo

drivers

src

login

tests

login

__init__.py

login.py

__init__.py

test_login.py

chromedriver

__init__.py

conftest.py

Development Environment

C:\> cd demo

C:\demo> python -m venv e2e

C:\demo> C:\demo\e2e\Scripts\activate.bat

(e2e) C:\demo>

(e2e) C:\demo> pip install -U pytest

(e2e) C:\demo> pip install -U selenium

Setup and teardown

# demo/tests/conftest.py

@pytest.fixture
def driver():
    pass

Setup and teardown

# demo/tests/conftest.py
import pytest
import selenium.webdriver

@pytest.fixture
def driver():
    pass

Setup and teardown

# demo/tests/conftest.py
import pytest
import selenium.webdriver

@pytest.fixture
def driver():
    driver = selenium.webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.get('https://www.saucedemo.com/')

Setup and teardown

# demo/tests/conftest.py
import pytest
import selenium.webdriver

@pytest.fixture
def driver():
    driver = selenium.webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.get('https://www.saucedemo.com/')
    yield driver
    driver.quit()

Login Page Object Model

# demo/src/login/login.py
class LoginPage():
  pass
def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored.
    time.sleep(3)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()
    
    # First click does not work sometimes.
    login_btn.click()

Our good old friend...

Login Page Object Model

# demo/src/login/login.py
class LoginPage():
    def __init__(self, driver):
        self.driver = driver

Login Page Object Model

# demo/src/login/login.py
class LoginPage():
    def __init__(self, driver):
        self.driver = driver
    
    def login(self, username, password):
      pass
def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored.
    time.sleep(3000)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()
    
    # First click does not work sometimes.
    login_btn.click()

Our good old friend...

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.common.by import By
class LoginPage():

    USERNAME_FLD = (By.ID, 'user-name')
    PASSWORD_FLD = (By.ID, 'password')
    LOGIN_BTN = (By.XPATH, '//input[@value="LOGIN"]')

    def __init__(self, driver):
        self.driver = driver
    
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
    
    def _set_text(self, value, locator_type, locator):
        input_field = self.driver.find_element(locator_type, locator)
        input_field.clear()
        input_field.send_keys(value)

System Under Test (SUT)

<div class="login-box">
  <form>
    <input type="text" class="form_input" data-test="username" id="user-name" placeholder="Username" value="">
    <input type="password" class="form_input" data-test="password" id="password" placeholder="Password" value="">
    <input type="submit" class="btn_action" value="LOGIN">
  </form>
</div>

Login Tests

# demo/tests/login/test_login.py
def test_login_successful():
    pass

Login Tests

# demo/tests/login/test_login.py
import pytest

def test_login_successful():
    pass

Login Tests

# demo/tests/login/test_login.py
import pytest
from src.login.login import LoginPage

def test_login_successful():
    pass

Login Tests

# demo/tests/login/test_login.py
import pytest
from src.login.login import LoginPage

def test_login_successful(driver):
    login_page = LoginPage(driver)
    login_page.login(username="standard_user", password="secret_sauce")
def test_login_successful(driver):
    username_field = driver.find_element(By.ID, 'username')
    username_field.clear()
    username_field.send_keys('admin')
	
    # Unhelpful comment that most probably will be ignored.
    time.sleep(3)
    
    password_field = driver.find_element(By.ID, 'password')
    password_field.clear()
    password_field.send_keys('Test1234')

    login_btn = driver.find_element(By.XPATH, '//input[@value="LOGIN"]')
    login_btn.click()
    
    # First click does not work sometimes.
    login_btn.click()

Our good old friend...

# demo/tests/conftest.py
import pytest
import selenium.webdriver

@pytest.fixture
def driver():
    driver = selenium.webdriver.Chrome()
    driver.implicitly_wait(10)
    driver.get('https://www.saucedemo.com/')
    yield driver
    driver.quit()

Setup and teardown

System Under Test (SUT)

<h3 data-test="error">
  <button class="error-button">
    <svg aria-hidden="true" data-prefix="fas" data-icon="times-circle" class="svg-inline--fa fa-times-circle fa-w-16 fa-2x " role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z">
      </path>
    </svg>
  </button>Epic sadface: Username and password do not match any user in this service
</h3>

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.common.by import By
class LoginPage():

    USERNAME_FLD = (By.ID, 'user-name')
    PASSWORD_FLD = (By.ID, 'password')
    LOGIN_BTN = (By.XPATH, '//input[@value="LOGIN"]')

    def __init__(self, driver):
        self.driver = driver
    
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
    
    def _set_text(self, value, locator_type, locator):
        input_field = self.driver.find_element(locator_type, locator)
        input_field.clear()
        input_field.send_keys(value)

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.common.by import By
class LoginPage():

    USERNAME_FLD = (By.ID, 'user-name')
    PASSWORD_FLD = (By.ID, 'password')
    LOGIN_BTN = (By.XPATH, '//input[@value="LOGIN"]')
    ERROR_MESSAGE = (By.XPATH, '//*[@data-test="error"]')
    EXPECTED_ERROR_MESSAGE = 'Epic sadface: Username and password do not match any user in this service'
    
    def __init__(self, driver):
        self.driver = driver
    
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
    
    def _set_text(self, value, locator_type, locator):
        input_field = self.driver.find_element(locator_type, locator)
        input_field.clear()
        input_field.send_keys(value)

Login Page Object Model

# demo/src/login/login.py
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
    
    def _set_text(self, value, locator_type, locator):
        input_field = self.driver.find_element(locator_type, locator)
        input_field.clear()
        input_field.send_keys(value)
    
    def _get_text(self, locator_type, locator):
        return self.driver.find_element(locator_type, locator).text

Login Page Object Model

# demo/src/login/login.py
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
    
    def _set_text(self, value, locator_type, locator):
        input_field = self.driver.find_element(locator_type, locator)
        input_field.clear()
        input_field.send_keys(value)
    
    def _get_text(self, locator_type, locator):
        return self.driver.find_element(locator_type, locator).text


class LoginError(Exception):
    pass

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
...
    def _get_text(self, locator_type, locator):
        return self.driver.find_element(locator_type, locator).text

    def _check_login_successful(self):
        try:
            WebDriverWait(self.driver, WAIT_FOR_ELEMENT).until(
                expected_conditions.invisibility_of_element_located(self.LOGIN_BTN))
        except:
            WebDriverWait(self.driver, WAIT_FOR_ELEMENT).until(
                expected_conditions.visibility_of_element_located(self.ERROR_MESSAGE))
            error_message = self._get_text(*self.ERROR_MESSAGE)
            raise LoginError(error_message)

class LoginError(Exception):
    pass

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
...
WAIT_FOR_ELEMENT = 2
...
    def _check_login_successful(self):
        try:
            WebDriverWait(self.driver, WAIT_FOR_ELEMENT).until(
                expected_conditions.invisibility_of_element_located(self.LOGIN_BTN))
        except:
            WebDriverWait(self.driver, WAIT_FOR_ELEMENT).until(
                expected_conditions.visibility_of_element_located(self.ERROR_MESSAGE))
            error_message = self._get_text(*self.ERROR_MESSAGE)
            raise LoginError(error_message)

class LoginError(Exception):
    pass

Login Page Object Model

# demo/src/login/login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions

WAIT_FOR_ELEMENT = 2

class LoginPage():

    USERNAME_FLD = (By.ID, 'user-name')
    PASSWORD_FLD = (By.ID, 'password')
    LOGIN_BTN = (By.XPATH, '//input[@value="LOGIN"]')
    ERROR_MESSAGE = (By.XPATH, '//*[@data-test="error"]')
    EXPECTED_ERROR_MESSAGE = 'Epic sadface: Username and password do not match any user in this service'
    
    def __init__(self, driver):
        self.driver = driver
    
    def login(self, username, password):
        self._set_text(username, *self.USERNAME_FLD)
        self._set_text(password, *self.PASSWORD_FLD)
        self.driver.find_element(*self.LOGIN_BTN).click()
        self._check_login_successful()

Login Tests

# demo/tests/login/test_login.py
import pytest
from src.login.login import LoginPage

def test_login_successful(driver):
    login_page = LoginPage(driver)
    login_page.login(username="standard_user", password="secret_sauce")

Login Tests

# demo/tests/login/test_login.py
import pytest
from src.login.login import LoginPage

def test_login_successful(driver):
    login_page = LoginPage(driver)
    login_page.login(username="standard_user", password="secret_sauce")

def test_login_unsuccessful(driver):
    login_page = LoginPage(driver)
    with pytest.raises(LoginError):
        login_page.login(username="locked_out_user", password="secret_sauce")

Last but not least

POM Repository

Tests

Webdriver

Base Framework

Monolithic Architecture

System Under Test

Last but not least

POM Repository

Webdriver

Test Env Creator

"Services" Architecture

System Under Test

Reporting Tool

Custom Framework Glue Code

Tests

Conclusion

Let computers do what they are good at…

Freeing up humans to create…

to dream…

 to change the world!

Thank you!

Test Automation Symphony

By Alejandro Serrano

Test Automation Symphony

Hello Test Automation!

  • 373