[Science, Technology, Engineering, Arts and Maths]

Pytest & Selenium – use same tests for LIVE, TEST and DEV

Most web platforms these days require at least 3 instances; the production/LIVE version, a test version and a development version. No one wants to have to write and maintain a separate suite of unit tests for each of these. Thankfully with pytest you can rely on a mix of fixtures and parameters. Before anything else you’re going to want to ensure you have all your variables for URLS, usernames and passwords setup in your .env file. For my example that might have a format similar to this:
APPLY_TEST_URL = "https://test_site.ie"
APPLY_TRAIN_URL = "https://train_site.ie"
APPLY_LIVE_URL = "https://live_site.ie"
APPLY_TEST_EMAIL = "testuser@there.com"
APPLY_TRAIN_EMAIL = "trainuser@there.com"
APPLY_LIVE_EMAIL = "liveuser@there.com"
APPLY_TEST_PASSWORD = "password"
APPLY_TRAIN_PASSWORD = "password"
APPLY_LIVE_PASSWORD = "password"
# settings for sending email with attached test report
EMAIL_SMTP_FROM = ""
EMAIL_REPORT_TO = ""
EMAIL_SMTP_SERVER = ""
EMAIL_SMTP_SERVER_PORT = ""
REPORT_DIR = "the directory where you want to output the error report to"
Next up, you are going to want to build your conftest.py file. In this version I’m generating a screenshot for any failed tests, embedding these (as Base64) in the pytest-html report being generated and emailing the lot to a specific email address if a fail occures.
import base64
import os
import smtplib
from datetime import datetime
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
from pathlib import Path
import arrow
import pytest
from dotenv import find_dotenv, load_dotenv
from selenium import webdriver
driver = None
# send the email with the report
def send_mail(message, file_name=None):
load_dotenv(find_dotenv())
msg = MIMEMultipart()
msg["From"] = os.getenv("EMAIL_SMTP_FROM")
msg["To"] = os.getenv("EMAIL_REPORT_TO")
msg["Date"] = formatdate(localtime=True)
msg["Subject"] = "Failing Selenium tests"
msg.attach(MIMEText(message))
# add attatchment
part = MIMEBase("application", "octet-stream")
with open(file_name, "rb") as file:
part.set_payload(file.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition", "attachment; filename={}".format(Path(file_name).name)
)
msg.attach(part)
smtp = smtplib.SMTP(
os.getenv("EMAIL_SMTP_SERVER"), os.getenv("EMAIL_SMTP_SERVER_PORT")
)
# Send the mail
smtp.sendmail(
os.getenv("EMAIL_SMTP_FROM"), os.getenv("EMAIL_REPORT_TO"), msg.as_string()
)
smtp.quit()
# set the report title
def pytest_html_report_title(report):
now = datetime.now()
report.title = "Selenium automated tests. Generated: " + now.strftime(
"%d-%m-%Y, %H:%M"
)
@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
# set custom options - ignore command line
now = datetime.now()
# custom report file
load_dotenv(find_dotenv())
report = os.getenv("REPORT_DIR") + "report_" + now.strftime("%d%m%y_%H%M") + ".html"
# adjust plugin options
config.option.htmlpath = report
config.option.self_contained_html = True
def pytest_sessionfinish(session, exitstatus):
if exitstatus == pytest.ExitCode.TESTS_FAILED:
now = datetime.now()
send_mail(
"This is an automated email from the scheduled Selenium tests. "
+ "One or more tests have failed. Please view the attached report for specific details."
+ "\nThis email was generated on "
+ now.strftime("%d/%m/%Y at %H:%M"),
file_name=session.config.option.htmlpath,
)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
pytest_html = item.config.pluginmanager.getplugin("html")
outcome = yield
report = outcome.get_result()
extra = getattr(report, "extra", [])
if report.when == "call" or report.when == "setup":
xfail = hasattr(report, "wasxfail")
if (report.skipped and xfail) or (report.failed and not xfail):
screenshot_file = "reports\\" + report.nodeid.replace("::", "_") + ".png"
driver.get_screenshot_as_file(screenshot_file)
if screenshot_file:
# embed screenshot as base64 image so we can include in email as single file
screenshot = open(screenshot_file, "rb").read()  # read bytes from file
screenshot_base64 = base64.b64encode(
screenshot
)  # encode to base64 (bytes)
screenshot_base64 = (
screenshot_base64.decode()
)  # convert bytes to string
html = (
'<div><img src="data:image/jpeg;base64, '
+ screenshot_base64
+ '" alt="screenshot" style="width:50%;" align="right"/></div>'
)
extra.append(pytest_html.extras.html(html))
report.extra = extra
@pytest.fixture(scope="function", autouse=True)
def browser():
global driver
if driver is None:
options = webdriver.ChromeOptions()
# remove the GUI pop-ups with headless
options.add_argument("--headless")
# remove annoying message to command line: "DevTools listening..."
options.add_experimental_option("excludeSwitches", ["enable-logging"])
driver = webdriver.Chrome(options=options)
return driver
Finally you will need to build the test(s), with any additional setup you think necessary. In the example below the @pytest.mark.parametrize is the important bit. This is the code that tells the test to rerun for each parameter in the list. The test below is a very simple check to ensure that either the site is returning a message saying “Online applications are now closed” or login has been successfully achieved.
import os
import pytest
from dotenv import find_dotenv, load_dotenv
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
@pytest.fixture
def setup_module(request, browser):
driver = browser
load_dotenv(find_dotenv())
url = os.getenv("APPLY_" + request.param + "_URL")
# call the site
driver.get(url)
driver.set_window_size(1100, 1000)
try:
# login
driver.find_element(By.XPATH, "//input[@type='email']").send_keys(
os.getenv("APPLY_" + request.param + "_EMAIL")
)
driver.find_element(By.XPATH, "//input[@type='password']").send_keys(
os.getenv("APPLY_" + request.param + "_PASSWORD")
)
driver.find_element(By.XPATH, "//input[@value='Login']").click()
yield driver
except NoSuchElementException:
yield driver
@pytest.mark.parametrize("setup_module", ["TEST", "TRAIN", "LIVE"], indirect=True)
def test_login_has_home_link(setup_module):
# case where the applications site is not allowing applications and using a placeholder
elements_applications_off = setup_module.find_elements(
By.XPATH, "//h2[text()='Online applications are now closed']"
)
# case where the applications site is up and running
elements_applictions_on = setup_module.find_elements(By.LINK_TEXT, "Log Off")
assert len(elements_applications_off) == 1 or len(elements_applictions_on) == 1