pytest Configuration — conftest.py and pytest.ini

7 min read

pytest is the test runner for this course. It's simpler than unittest for mobile tests: fixtures replace setUp/tearDown classes, parametrize handles data-driven tests, and the plugin ecosystem covers reporting, parallelism, and retry. This lesson covers the pytest features you'll use most.

Test functions and test classes

pytest discovers tests by file name (test_*.py), function name (test_*), and optionally class name (Test*):

# test_login.py
 
def test_login_with_valid_credentials(driver):
    # driver is a fixture injected by pytest
    from pages.login_page import LoginPage
    page = LoginPage(driver)
    home = page.login("standard_user", "secret_sauce")
    assert home.is_displayed()
 
class TestLoginValidation:
    def test_empty_username_shows_error(self, driver):
        from pages.login_page import LoginPage
        page = LoginPage(driver)
        page.login("", "secret_sauce")
        assert "Username is required" in page.get_error_message()

Both forms work. Plain functions are simpler; classes let you group related tests and share class-level state.

Fixtures

Fixtures are pytest's dependency injection system. A fixture is a function decorated with @pytest.fixture:

# conftest.py
import pytest
from appium import webdriver
from appium.options import UiAutomator2Options
 
@pytest.fixture
def driver():
    options = UiAutomator2Options()
    options.device_name = "emulator-5554"
    options.app = "apps/app-debug.apk"
    options.auto_grant_permissions = True
 
    d = webdriver.Remote("http://127.0.0.1:4723", options=options)
    yield d           # test runs here
    d.quit()          # teardown: always runs, even if test fails

Any test function that declares driver as a parameter receives a fresh driver for each test. The yield separates setup (above) from teardown (below). Code below yield runs after the test, regardless of pass/fail.

Fixture scope

@pytest.fixture(scope="function")  # default — new fixture for every test
@pytest.fixture(scope="class")     # shared within a test class
@pytest.fixture(scope="module")    # shared within a test file
@pytest.fixture(scope="session")   # shared for the entire test run

For mobile, function scope means a new Appium session per test — slow but clean. module scope means one session per test file — faster, but tests must not leave the app in a broken state. Choose based on how independent your tests need to be.

Assertions

pytest uses Python's built-in assert with introspection — you get helpful failure messages without needing assertion libraries:

assert home.is_displayed(), "Home page should be visible after login"
 
# Comparison
product_count = home.get_product_count()
assert product_count >= 6, f"Expected at least 6 products, got {product_count}"
 
# In list
assert "Backpack" in home.get_product_names()
 
# Exception
import pytest
with pytest.raises(ValueError, match="Invalid input"):
    parse_price("not-a-price")

Running tests

# Run all tests
pytest
 
# Run a specific file
pytest tests/test_login.py
 
# Run a specific test
pytest tests/test_login.py::test_login_with_valid_credentials
 
# Run a specific class
pytest tests/test_login.py::TestLoginValidation
 
# Run tests matching a keyword
pytest -k "login"
 
# Run with verbose output
pytest -v
 
# Show print output
pytest -s
 
# Run and stop on first failure
pytest -x

Markers

Mark tests to control which run:

import pytest
 
@pytest.mark.smoke
def test_standard_user_login(driver):
    ...
 
@pytest.mark.regression
def test_locked_out_user(driver):
    ...

Register markers in pytest.ini to avoid warnings:

[pytest]
markers =
    smoke: core functionality tests
    regression: full test suite
    slow: tests that take more than 30 seconds

Run by marker:

pytest -m smoke
pytest -m "regression and not slow"

conftest.py — shared fixtures and hooks

conftest.py in the project root is automatically loaded by pytest. Fixtures defined there are available to all test files. Multiple conftest.py files can exist — pytest loads them hierarchically (root → subdirectory):

conftest.py          ← available to all tests
tests/
    conftest.py      ← available to tests in tests/ only
    test_login.py

Use conftest.py for:

  • Shared fixtures (driver, pages)
  • pytest hooks (e.g., screenshot on failure)
  • Common test data

The yield fixture pattern for screenshots

Capture a screenshot when a test fails using a fixture:

@pytest.fixture
def driver(request):
    options = UiAutomator2Options()
    options.device_name = "emulator-5554"
    options.app = "apps/app-debug.apk"
 
    d = webdriver.Remote("http://127.0.0.1:4723", options=options)
    yield d
 
    # Teardown: check if the test failed
    if request.node.rep_call.failed:
        screenshot_path = f"screenshots/{request.node.name}.png"
        d.get_screenshot_as_file(screenshot_path)
 
    d.quit()

The request.node.rep_call.failed check requires the pytest_runtest_makereport hook (covered in Chapter 5's reporting lesson).

// tip to track lessons you complete and pick up where you left off across devices.