Playwright with Python - A Quick Guide

lrenzi

Luciano Renzi

Posted on June 14, 2024

Playwright with Python - A Quick Guide

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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/'
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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'
    }
})
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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():
    # ...
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
lrenzi
Luciano Renzi

Posted on June 14, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Playwright with Python - A Quick Guide