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
- Download chrome webdriver and add it to the PATH env variable
__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!
- 460