Skip to content
CodeKruPro

Framework · Python

Test runner basics (TestNG or pytest)

Writing and running tests, assertions, setup and teardown.

Learn: Test runner basics in Python: pytest9 minute read

A test runner is the tool that finds your tests, prepares what they need, runs them and reports the results. Selenium only drives the browser; it has no idea what a test is. In Python automation the runner is almost always pytest. Interviewers start here because every later question, from page objects to parallel runs, assumes you know how pytest finds tests, what fixtures are, and how a test passes or fails.

How pytest finds tests

By default pytest collects files named test_*.py or *_test.py. Inside them it collects functions whose names start with test, and test methods inside classes whose names start with Test. Such a class must not define __init__ (the method that sets up a new object). When "my test does not run", the first thing to try is pytest --collect-only, which lists everything pytest found.

Assertions

An assertion is a check that fails the test when it is not true. pytest uses Python's plain assert:

def test_title(driver):
    driver.get("https://example.com")
    assert driver.title == "Example Domain", f"wrong page: {driver.current_url}"

pytest rewrites assert statements in test files, so a failure shows the values on both sides. That is why there are no assertEqual methods. A plain assert is a hard assertion: the test stops there. pytest has no soft assertions built in. You can collect failures in a list and assert once at the end, or use the pytest-check plugin (a plugin is an add-on package that you install separately). To check that code raises an error, use with pytest.raises(ValueError, match="invalid"):.

Fixtures

A fixture is a function that prepares something a test needs. The test asks for it by naming it as a parameter, and pytest calls the fixture and passes in the result. Code before yield is the set-up. yield hands the value to the test and pauses the fixture. Code after yield is the teardown (the clean-up step), and it runs after the test even when the test failed:

import pytest
from selenium import webdriver

@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()

Fixtures can use other fixtures, so a logged_in_page fixture can take driver and an api fixture. One catch: if set-up fails before it reaches yield, that fixture's teardown does not run. Keep risky set-up small.

Scopes

The scope decides how often pytest creates a fixture. The default, "function", gives every test its own copy. Wider scopes create it less often and share it between tests.

Nested boxes for the pytest fixture scopes session, package, module, class and function, with function as the default that gives each test a fresh browser.
Each scope sits inside the wider one. Function scope, the default, is the narrowest.

A wider scope saves time but shares state. A browser shared across a module carries cookies and page state from test to test. A configuration object or an API token is safe to share for the whole session. A fixture may use fixtures of the same or a wider scope, never a narrower one. autouse=True applies a fixture to every test it can reach without naming it.

conftest.py

Fixtures and hooks (functions that pytest calls at set moments, such as after each test) in a conftest.py file are available to every test in that folder and below, with no import. A typical project has a root conftest.py with the driver fixture, command-line options and report hooks, and deeper ones for fixtures that only one area needs.

parametrize

@pytest.mark.parametrize("user, ok", [
    ("admin", True),
    ("blocked", False),
])
def test_login(driver, user, ok):
    ...

Each row becomes its own test, reported with an id such as test_login[admin-True], so one failing row does not hide the others. Give readable ids with ids=[...] or pytest.param(..., id="blocked").

Why pytest rather than unittest

unittest ships with Python but needs classes, setUp methods and assert methods. pytest gives you plain assert, fixtures with scopes, parametrize, markers (labels such as @pytest.mark.smoke that let you choose tests), and plugins: pytest-xdist for parallel runs, pytest-html and allure-pytest for reports, pytest-rerunfailures for reruns and pytest-bdd for BDD. It also runs old unittest tests.

Command-line options you will use

pytest tests/test_login.py::test_valid   # one test
pytest -k "login and not admin"           # select by name
pytest -m smoke                           # select by marker
pytest -x                                 # stop at the first failure
pytest -v                                 # list each test and its result
pytest -s                                 # show print output at once
pytest --lf                               # rerun last run's failures
pytest --junitxml=results.xml             # results file for CI

The --junitxml file is a standard results format that CI servers (machines that run your tests automatically, for example after every code change) can read. Default options, test folders and registered markers go in pytest.ini or in the [tool.pytest.ini_options] table of pyproject.toml.

How to answer in an interview

For "what are fixtures?", define them in one sentence, show the yield driver fixture, then name the scopes and when you widen one. For example: "A fixture prepares what a test needs. My driver fixture opens Chrome, yields it and quits it, at function scope so every test gets a fresh browser. Config and the API token are session-scoped."

Common mistakes

  • Naming a file login_tests.py and wondering why pytest skips it.
  • Using a module-scoped browser and getting tests that pass alone but fail together.
  • Copying the same fixture into many files instead of one conftest.py.
  • Saying pytest has built-in soft assertions. It does not.

Recap

pytest finds tests by name, runs them, and reports plain assert failures with both values. Fixtures prepare what tests need, and yield splits set-up from teardown. The scope controls how often a fixture is built, conftest.py shares fixtures without imports, and parametrize turns rows into separate tests.

10 questions0/10 solved

StatusQuestionDifficultyAsked in
Not startedWhere do shared pytest fixtures go?EasyAsked in: Cisco
Not startedWhich tests does pytest find?Easy
Pro+pytest fixtures, conftest.py and parametrizeMediumAsked in: Cisco
Pro+How often is a module-scoped fixture created?Easy-MediumAsked in: Cisco
Pro+Does fixture teardown run when the test fails?Easy-MediumAsked in: Cisco
Pro+Why pytest uses the plain assert statementEasyAsked in: Accenture, Capgemini, TCS +1
Pro+Hard and soft assertions in pytestMediumAsked in: Accenture, Capgemini, Wipro
Pro+Check that code raises an exception in pytestEasy-Medium
Pro+Why pytest for a Python test framework?MediumAsked in: Accenture, Capgemini, TCS +1
Pro+Useful pytest command-line optionsEasy-MediumAsked in: Accenture, Capgemini, Infosys
Not startedNot startedIn progressIn progressSolvedSolvedPro+Pro+

The first 2 questions of each topic are free. Pro+ opens every question.

Back to the plan