January 20, 2026
#4 - Pytest Concept
Part 4 of 5 · API Testing | Pytest & requests
pytest is one of the most widely adopted testing frameworks in the Python ecosystem. It provides a clean, expressive, and highly extensible way to write tests — ranging from simple unit tests to large-scale API and integration test suites.
Tests are written as regular functions and classes, assertions use native Python syntax, and structure is driven by conventions rather than rigid frameworks. This makes tests easy to read, easy to maintain, and straightforward to scale as projects grow.
conftest
conftest.py is a pytest configuration and dependency injection file. It's used to:
- Define shared fixtures
- Configure test-wide behavior
- Expose fixtures without explicit imports
- Control fixture visibility by directory scope
Pytest automatically discovers conftest.py files by walking up the directory tree.
Fixtures defined in tests/conftest.py are:
- Available to all tests under
tests/ - Imported implicitly
- Resolved via fixture dependency injection
conftest is used for
- Shared fixtures
- Environment and configuration setup
- Base URLs
- Environment selection (
--env=dev) - Auth tokens
- Sessions
- API client/sessions
- Pytest hooks and options
pytest_runtest_setuppytest_sessionstart
conftest should not be used for
- Test logic
- Assertions
- One-off fixtures used by a single test
- Business logic
Rules
- Fixtures apply downward only
- The closest
conftest.pyis used - Enables bounded context per domain
tests/
├── conftest.py # global fixtures
├── users/
│ ├── conftest.py # user-specific fixtures
│ └── test_users.pyFixture
A fixture is a dependency provider — it handles setup before a test runs, reuse of that setup across many tests, and teardown after the test.
Key idea:
- A test does not create the user
- A test declares what it needs
- pytest injects the dependency
- Fixtures can depend on other fixtures
Fixture scope
function(default) — per test functionclass— per test classmodule— per test filepackage— per foldersession— entire test run
Setup and teardown using a yield fixture
@pytest.fixture
def temp_user(user_client):
user = user_client.create_user()
yield user
user_client.delete_user(user["id"])Execution order:
- Code before
yield→ setup - Test runs
- Code after
yield→ teardown (even if the test fails)
Fixture parametrization
Fixtures can produce multiple variants — the same test runs multiple times, once per fixture value, keeping test logic cleanly separated from test data.
@pytest.fixture(params=["admin", "user"])
def role(request):
return request.paramdef test_access(role):
...Fixture override and conftest
conftest.py is the central place for shared fixtures — no imports needed, scoped by directory tree.
Overriding fixtures
- A lower-level
conftest.pyoverrides higher ones - Useful for environment-specific behavior
- Useful for test-specific customization
Pytest execution
At a high level, pytest follows a simple flow:
- Find tests
- Pytest looks for test files, functions, and classes that follow its naming rules
- Files starting with
test_or ending with_test.py - Functions starting with
test_ - Classes starting with
Test
- Prepare test requirements
- If tests use fixtures, pytest figures out what each test needs and builds a dependency plan. Fixtures are set up in the correct order before the test runs.
- Run the tests
- Pytest executes the tests one by one. During execution: fixtures are created, test code runs, assertions are evaluated, and if something goes wrong, the test is marked as failed or errored.
- Report the results
- After execution, pytest shows a summary: passed tests, failed tests, and skipped tests. For failures, pytest also shows error messages and stack traces to help with debugging.
Originally published on Hashnode.