Pytest for TestNG users: every feature with examples
Each section shows the TestNG concept, the pytest equivalent, and an explanation.
0. Setup and project structure
pip install pytest pytest-dependency pytest-order pytest-timeout pytest-xdist pytest-check pytest-repeat pytest-html
project/
├── conftest.py # shared fixtures (like a base test class)
├── pytest.ini # config (like testng.xml)
└── tests/
├── test_basic.py
└── test_login.py
Pytest auto-discovers files named test_*.py or *_test.py, and inside them, functions named test_* and classes named Test*. No annotation is needed for @Test.
Pytest plugins and their uses
Package | Purpose |
|---|---|
| Main Python testing framework |
| Manage dependencies between test cases |
| Control test execution order |
| Set timeout for test execution |
| Execute tests in parallel |
| Perform multiple assertions in one test |
| Repeat test cases multiple times |
| Generate HTML test reports |
1. @Test: basic test
# tests/test_basic.py
def test_addition():
assert 1 + 1 == 2
def test_string():
assert "hello".upper() == "HELLO"
Explanation:
- Any function starting with
test_is a test. - Use the plain
assertkeyword instead ofAssert.assertEquals. If it fails, pytest shows both values and the exact expression. - A test passes if no exception is raised.
2. Grouping tests in a class
class TestCalculator:
def test_add(self):
assert 2 + 3 == 5
def test_multiply(self):
assert 2 * 3 == 6
Explanation:
- This is like a TestNG test class. The class name must start with
Testand must not have an__init__. - Classes are optional. Use them to group related tests and to apply class-level fixtures or markers.
3. @BeforeMethod / @AfterMethod: function-scoped fixture
# conftest.py
import pytest
@pytest.fixture
def login():
print("\n[Setup] Logging in")
yield "user_session" # test runs here
print("[Teardown] Logging out")
# tests/test_login.py
def test_dashboard(login):
print("Running dashboard test with", login)
assert login == "user_session"
def test_profile(login):
assert True
Explanation:
- Code before
yieldis the setup (@BeforeMethod). Code afteryieldis the teardown (@AfterMethod). - A test requests a fixture by adding its name as a parameter. Pytest injects it automatically.
- The value after
yieldis what the test receives. - Default scope is
function, so this runs once per test, here twice. - Run with
pytest -sto see the prints.
4. @BeforeClass / @AfterClass: class or module scope
# conftest.py
@pytest.fixture(scope="class")
def db_connection():
print("\n[Setup] Connect DB")
yield "db"
print("[Teardown] Close DB")
import pytest
@pytest.mark.usefixtures("db_connection")
class TestUsers:
def test_one(self):
assert True
def test_two(self):
assert True
Explanation:
scope="class"runs setup once before the first test in the class and teardown after the last.scope="module"does the same per file, which is like@BeforeTestin a way.@pytest.mark.usefixtures(...)applies a fixture without passing it as an argument. Use it when the test doesn't need the returned value.
5. @BeforeSuite / @AfterSuite: session scope
# conftest.py
@pytest.fixture(scope="session")
def browser():
print("\n[Suite Setup] Launch browser")
yield "chrome"
print("[Suite Teardown] Quit browser")
def test_a(browser):
assert browser == "chrome"
def test_b(browser):
assert browser == "chrome"
Explanation:
scope="session"runs once for the entire pytest run, however many files there are.- Put it in
conftest.pyso every test file can use it without importing. Pytest loadsconftest.pyautomatically. - Here the browser launches once, both tests share it, and it quits at the end.
6. Auto-running fixtures (implicit @BeforeMethod)
# conftest.py
@pytest.fixture(autouse=True)
def log_test_name(request):
print(f"\n>>> Starting {request.node.name}")
yield
print(f"<<< Finished {request.node.name}")
Explanation:
autouse=Truemakes the fixture run for every test in scope, with no need to request it. This is the closest match to TestNG's implicit@BeforeMethod.requestis a built-in fixture that gives info about the running test.
7. @DataProvider: parametrize
import pytest
@pytest.mark.parametrize("username, password, expected", [
("admin", "admin123", True),
("guest", "wrong", False),
("", "", False),
])
def test_login(username, password, expected):
result = (username == "admin" and password == "admin123")
assert result == expected
Explanation:
- The first argument is a comma-separated string of parameter names. The second is a list of tuples, one per run.
- This runs 3 separate tests, each reported on its own.
- Give readable IDs with
ids=["valid", "invalid", "empty"]inside the decorator.
Data from a function (like a @DataProvider method):
def get_data():
return [(1, 2, 3), (2, 3, 5), (10, 5, 15)]
@pytest.mark.parametrize("a, b, total", get_data())
def test_sum(a, b, total):
assert a + b == total
Stacking decorators gives a cross product:
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_combo(x, y): # runs 4 times: (1,10) (2,10) (1,20) (2,20)
assert x * y > 0
8. groups: markers
import pytest
@pytest.mark.smoke
def test_homepage():
assert True
@pytest.mark.regression
def test_checkout():
assert True
@pytest.mark.smoke
@pytest.mark.regression
def test_login():
assert True
Register markers in pytest.ini (otherwise you get warnings):
[pytest]
markers =
smoke: quick sanity checks
regression: full regression suite
pytest -m smoke
pytest -m "smoke and regression"
pytest -m "regression and not smoke"
Explanation:
- A marker is a tag. One test can have many.
-maccepts boolean expressions (and,or,not), so it's more flexible than TestNG groups.- You can mark a whole class (
@pytest.mark.smokeaboveclass TestX) and every test inside inherits it.
9. enabled = false: skip and conditional skip
import pytest, sys
@pytest.mark.skip(reason="Feature not ready")
def test_new_feature():
...
@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
def test_linux_only():
...
def test_runtime_skip():
if not some_condition():
pytest.skip("Condition not met") # skip from inside the test
Explanation:
skipalways skips.skipifskips when the condition is true.pytest.skip()skips dynamically at runtime.- Skipped tests show as
sin the output, not as failures.
Bonus: xfail (expected failure, a known bug):
@pytest.mark.xfail(reason="Bug #123")
def test_known_bug():
assert 1 == 2 # reported as XFAIL, not a failure
If it unexpectedly passes, it shows as XPASS. Add strict=True to turn that into a failure.
10. expectedExceptions: pytest.raises
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_exception_message():
with pytest.raises(ValueError, match="invalid literal"):
int("abc")
def test_inspect_exception():
with pytest.raises(KeyError) as exc_info:
{}["missing"]
assert "missing" in str(exc_info.value)
Explanation:
- The test passes only if the expected exception is raised inside the
withblock. match=checks the message against a regex, like TestNG'sexpectedExceptionsMessageRegExp.exc_infolets you inspect the exception afterwards.
11. timeOut: pytest-timeout
import pytest, time
@pytest.mark.timeout(2)
def test_must_finish_fast():
time.sleep(5) # FAILS: exceeds 2 seconds
Or set a global timeout in pytest.ini:
[pytest]
timeout = 30
Explanation:
- It needs
pip install pytest-timeout. - The test fails if it runs longer than the limit.
- A marker on a test overrides the global value.
12. priority: pytest-order
import pytest
@pytest.mark.order(2)
def test_second():
print("second")
@pytest.mark.order(1)
def test_first():
print("first")
@pytest.mark.order("last")
def test_last():
print("last")
Explanation:
- By default pytest runs tests in the order they appear in the file, not alphabetically as TestNG does.
pytest-orderlets you set explicit order with numbers,"first", or"last".- Lower numbers run first, which matches TestNG's
priority. - Good practice is to keep tests independent and avoid relying on order.
13. dependsOnMethods: pytest-dependency
import pytest
@pytest.mark.dependency()
def test_create_user():
assert True
@pytest.mark.dependency(depends=["test_create_user"])
def test_update_user():
assert True
@pytest.mark.dependency(depends=["test_update_user"])
def test_delete_user():
assert True
Explanation:
test_update_userruns only iftest_create_userpassed. Otherwise it is skipped, as in TestNG.- Without
pytest-dependency, you'd use fixtures to share state instead, which is the more "pytest-native" way. - Dependencies across classes or modules need a
scope:@pytest.mark.dependency(depends=["..."], scope="module").
14. invocationCount: pytest-repeat
def test_flaky_check():
assert True
pytest --count=5 # run each test 5 times
pytest --count=5 -x # stop at first failure
Or per-test:
import pytest
@pytest.mark.repeat(3)
def test_three_times():
assert True
Explanation:
- It needs
pip install pytest-repeat. - It's useful for catching flaky tests.
15. parallel: pytest-xdist
pytest -n 4 # 4 worker processes
pytest -n auto # one per CPU core
pytest -n 4 --dist loadscope # keep tests of the same class/module on one worker
Explanation:
- It needs
pip install pytest-xdist. - It uses separate processes, not threads, so tests must be independent and not share mutable global state.
- A
sessionfixture runs once per worker, not once overall. Keep this in mind for things like database setup. --dist loadscopekeeps a class's tests together, similar toparallel="classes".
16. SoftAssert: pytest-check
from pytest_check import check
def test_soft_assertions():
check.equal(1, 1, "first")
check.equal(2, 3, "second fails") # recorded, test continues
check.is_true(False, "third fails") # recorded, test continues
# test fails at the end, reporting both failures
Or with the context manager:
from pytest_check import check
def test_with_context():
with check:
assert 1 == 2
with check:
assert 3 == 4
Explanation:
- Normal
assertstops the test at the first failure.checkcollects all failures and reports them together, likesoftAssert.assertAll(). - You don't need to call
assertAll(). Pytest does it at the end of the test.
17. Sharing data between tests and fixtures with parameters
@pytest.fixture(params=["chrome", "firefox", "edge"])
def browser(request):
return request.param
def test_open_site(browser):
print("Testing on", browser)
Explanation:
- This is like TestNG's
@Parameterscombined with a data provider at the fixture level. - The test runs once per param (3 times here).
- It's ideal for cross-browser testing.
18. testng.xml: configuration
pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
markers =
smoke: quick sanity checks
regression: full regression suite
addopts = -v --tb=short --html=report.html --self-contained-html
timeout = 60
Explanation:
testpathssets where to look for tests.addoptsholds default CLI flags, so you don't retype them.- You can define "suites" with commands, folders, or markers:
pytest tests/api # an "API suite" by folder
pytest -m smoke # a "smoke suite" by marker
pytest tests/test_login.py tests/test_cart.py # a custom list
19. Reporting
pytest --html=report.html --self-contained-html # pytest-html
pytest --junitxml=results.xml # JUnit XML (CI tools)
For Allure (similar to rich TestNG reports):
pip install allure-pytest
pytest --alluredir=allure-results
allure serve allure-results
20. Useful run commands
| Command | Meaning |
|---|---|
pytest | Run all tests |
pytest -v | Verbose output |
pytest -s | Show print() output |
pytest -x | Stop at first failure |
pytest --maxfail=3 | Stop after 3 failures |
pytest --lf | Re-run only last failed tests |
pytest -k "login and not admin" | Select by name expression |
pytest file.py::test_name | Run one test |
pytest file.py::TestClass::test_name | Run one test in a class |
pytest --collect-only | List tests without running them |
21. Complete mini example combining everything
# conftest.py
import pytest
@pytest.fixture(scope="session")
def browser():
print("\n[Suite] start browser")
yield "chrome"
print("[Suite] quit browser")
@pytest.fixture
def login(browser):
print("[Method] login")
yield browser
print("[Method] logout")
# tests/test_shop.py
import pytest
from pytest_check import check
@pytest.mark.smoke
@pytest.mark.order(1)
@pytest.mark.dependency()
def test_login(login):
assert login == "chrome"
@pytest.mark.regression
@pytest.mark.dependency(depends=["test_login"])
@pytest.mark.parametrize("item, qty", [("book", 1), ("pen", 5)])
def test_add_to_cart(login, item, qty):
assert qty > 0
@pytest.mark.timeout(3)
def test_checkout(login):
with pytest.raises(ValueError):
int("not a number")
def test_validations(login):
check.equal(1, 1)
check.is_true(True)
pytest -m smoke -v -s
pytest -n 2 -m regression
Key mindset shifts from TestNG
- Fixtures replace annotations. Setup and teardown are functions you request by name, not hooks tied to a class.
conftest.pyreplaces base classes. Shared setup lives there, with no inheritance needed.- Plain
assert. No assertion library is needed. - Markers replace groups and
enabled. They are flexible tags filtered from the command line. - Plugins fill the gaps. Dependency, ordering, timeout, repeat, parallel, and soft asserts each come from a small plugin.

No comments:
Post a Comment