In this guide, we'll see how to set up a basic Playwright project using Python and Pytest. Then, how to implement the Page Object Pattern and a few other things.
This guide requires a basic knowledge of Python.
Preconditions
The packages required are:
playwright
pytest-playwright
pytest-xdist
The folder structure
The base folder structure for our project:
/pages
/tests
conftest.py
requirements.txt
Installation
We add the required packages to the requirements.txt file:
requirements.txt
playwright>=1.44.0
pytest-playwright>=0.5.0
pytest-xdist>=3.6.1
Then we run the following command:
pip install -r requirements.txt
Note: instead of pip
, pip3
might be required depending on the OS.
Then:
playwright install
This command will install the required browsers.
Adding a Basic Test
A basic login test using Playwright:
/tests/test_login.py
from playwright.sync_api import Page, expect
def test_login_success(page: Page):
page.goto('https://react-redux.realworld.io/#/login')
page.get_by_placeholder('Email').type('test_playwright_login@test.com')
page.get_by_placeholder('Password').type('Test123456')
page.get_by_role('button', name='Sign in').click()
expect(page.get_by_role('link', name='test_playwright_login')).to_be_visible()
Running this test
This runs a single test in headed mode. Playwright runs in headless mode by default.
pytest -k test_login_success --headed
Running all the tests
pytest
Selecting the browsers
pytest --browser webkit --browser firefox
Implementing the Page Object Pattern
To start using the POM we add:
pages/login_page.py
from playwright.sync_api import Page
class Login:
def __init__(self, page: Page):
self.page = page
self.email_input = page.get_by_placeholder('Email')
self.password_input = page.get_by_placeholder('Password')
self.signin_button = page.get_by_role('button', name='Sign in')
def goto(self):
self.page.goto('/#/login')
pages/navbar_page.py
from playwright.sync_api import Page
class Navbar:
def __init__(self, page: Page):
self.page = page
def user_link(self, username: str):
return self.page.get_by_role('link', name=username)
We add the base URL of our app to the conftest.py as a fixture like this:
conftest.py
import pytest
@pytest.fixture(scope='session')
def base_url():
return 'https://react-redux.realworld.io/'
And now the test looks like this:
tests/test_login.py
from playwright.sync_api import Page, expect
from pages.login_page import Login
from pages.navbar_page import Navbar
def test_login_success(page: Page):
login = Login(page)
navbar = Navbar(page)
login.goto()
login.email_input.type('test_playwright_login@test.com')
login.password_input.type('Test123456')
login.signin_button.click()
expect(navbar.user_link('test_playwright_login')).to_be_visible()
Using Pytest fixtures to Instantiate the Page Objects
Instead of instantiating the page objects in each test, we use Pytest fixtures.
We add the following to conftest.py
conftest.py
import pytest
from playwright.sync_api import Page
from pages.login_page import Login
from pages.navbar_page import Navbar
@pytest.fixture(scope='session')
def base_url():
return 'https://react-redux.realworld.io/'
@pytest.fixture
def page(page: Page) -> Page:
timeout = 10000
page.set_default_navigation_timeout(timeout)
page.set_default_timeout(timeout)
return page
@pytest.fixture
def login(page) -> Login:
return Login(page)
@pytest.fixture
def navbar(page) -> Navbar:
return Navbar(page)
Note: we use the "page" fixture to define timeouts.
Now the test looks like this:
tests/test_login.py
from playwright.sync_api import expect
def test_login_success(login, navbar):
login.goto()
login.email_input.type('test_playwright_login@test.com')
login.password_input.type('Test123456')
login.signin_button.click()
expect(navbar.user_link('test_playwright_login')).to_be_visible()
Removing User Data Values from the Test
We create a users.py file to store user's data, just one user for now. The DictObject is a utility class to access dictionary values using object notation. It could be moved elsewhere, but for now, we keep it here.
users.py
import json
class DictObject(object):
def __init__(self, dict_):
self.__dict__.update(dict_)
@classmethod
def from_dict(cls, d):
return json.loads(json.dumps(d), object_hook=DictObject)
USERS = DictObject.from_dict({
'user_01': {
'username': 'test_playwright_login',
'email': 'test_playwright_login@test.com',
'password': 'Test123456'
}
})
And we update the test:
tests/test_login.py
from playwright.sync_api import expect
from users import USERS
def test_login_success(login, navbar):
login.goto()
login.email_input.type(USERS.user_01.email)
login.password_input.type(USERS.user_01.password)
login.signin_button.click()
expect(navbar.user_link(USERS.user_01.username)).to_be_visible()
Running tests in parallel
To run the tests in parallel we use pytest-xdist. We should already have it installed by this point.
pytest -n 5
Where -n
is the number of workers.
Managing Environment Data
Usually, we want the tests to run in different environments. So we want to set the base URL based on the selected env.
We add these changes to the conftest.py file:
conftest.py
def pytest_addoption(parser):
parser.addoption("--env", action="store", default="staging")
@pytest.fixture(scope='session', autouse=True)
def env_name(request):
return request.config.getoption("--env")
@pytest.fixture(scope='session')
def base_url(env_name):
if env_name == 'staging':
return 'https://react-redux.realworld.io/'
elif env_name == 'production':
return 'https://react-redux.production.realworld.io/'
else:
exit('Please provide a valid environment')
After this, we can pass as an argument the environment name like this:
pytest --env staging
Defining Global Before and After Test
@pytest.fixture(scope="function", autouse=True)
def before_each_after_each(page: Page, base_url):
# The code here runs before each test
print(‘before the test runs’)
# Go to the starting url before each test.
page.goto(base_url)
yield
# This code runs after each test
print(‘after the test runs’)
This fixture can be added to the conftest.py and apply it to every test. Or it can be defined inside a single test module and be applied only to the tests inside that module.
Tagging the Tests
To tag the tests we can use Pytest marker feature.
import pytest
@pytest.mark.login
def test_login_success():
# ...
We must register our custom markers in the pytest.ini file (a new file added in the root folder).
pytest.ini
[pytest]
markers =
login: mark test as a login test.
slow: mark test as slow.
And to run a custom marker/tag we use:
pytest -m login
Tooling
Codegen
This generates a test capturing actions in real time. It's useful for generating locators and assertions. But if we are using POM, then the generated code needs to be refactored into it.
Playwright Inspector
This is a debugger util that enables running a test step by step, among other things.
Trace Viewer
The trace viewer records the result of a test so it can be reviewed later with a live preview of each action performed. This is super useful specially when running tests from CI.
Top comments (1)
Really usefull