FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Python Testing with pytest: Fixtures, Parametrize, Mocking and Best Practices

Master Python testing with pytest. Covers writing tests, fixtures and their scopes, parametrize, mocking with unittest.mock, testing async code, coverage, and test organization best practices.

Yusuf SeyitoğluMarch 17, 20260 views10 min read

Python Testing with pytest: Fixtures, Parametrize, Mocking and Best Practices

pytest is the standard testing framework for Python. Its simple syntax, powerful fixtures, and rich plugin ecosystem make it far more pleasant to work with than the built-in unittest. This guide teaches you to write tests that are fast, readable, and actually catch real bugs.

Getting Started

bash
pip install pytest pytest-cov pytest-asyncio pytest-mock
python
-- test_math.py def add(a, b): return a + b def divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / b -- Tests def test_add_positive_numbers(): assert add(1, 2) == 3 def test_add_negative_numbers(): assert add(-1, -2) == -3 def test_divide_correctly(): assert divide(10, 2) == 5.0 def test_divide_by_zero_raises(): with pytest.raises(ValueError, match="Cannot divide by zero"): divide(10, 0)

Run tests:

bash
pytest -- run all tests pytest test_math.py -- specific file pytest test_math.py::test_add -- specific test pytest -v -- verbose output pytest -k "divide" -- run tests matching pattern pytest --tb=short -- shorter traceback pytest -x -- stop on first failure pytest --cov=myapp --cov-report=html -- with coverage

Fixtures

Fixtures provide reusable setup/teardown for tests. pytest injects them by name:

python
import pytest from myapp.database import Database from myapp.models import User @pytest.fixture def db(): -- Setup database = Database(url="sqlite:///:memory:") database.create_tables() yield database -- provide to test -- Teardown (runs after test, even if test fails) database.drop_tables() database.close() @pytest.fixture def sample_user(db): -- Fixtures can depend on other fixtures user = User(username="alice", email="alice@example.com") db.save(user) return user def test_user_creation(db): user = User(username="bob", email="bob@example.com") db.save(user) assert db.get_user_by_email("bob@example.com") is not None def test_user_lookup(sample_user, db): found = db.get_user_by_email("alice@example.com") assert found.username == "alice"

Fixture Scopes

python
@pytest.fixture(scope="function") -- default: new instance per test def temp_dir(tmp_path): return tmp_path / "test_data" @pytest.fixture(scope="class") -- shared within a test class def db_connection(): conn = create_connection() yield conn conn.close() @pytest.fixture(scope="module") -- shared within a test module (file) def expensive_setup(): data = load_large_dataset() yield data @pytest.fixture(scope="session") -- shared across entire test session def app_client(): app = create_app(testing=True) with app.test_client() as client: yield client

Use wider scopes for expensive operations. session-scoped fixtures like database connections speed up test suites dramatically.

conftest.py

Place shared fixtures in conftest.py β€” pytest automatically discovers them:

python
-- tests/conftest.py import pytest from myapp import create_app from myapp.database import db as _db @pytest.fixture(scope="session") def app(): app = create_app("testing") return app @pytest.fixture(scope="session") def client(app): return app.test_client() @pytest.fixture(scope="function") def db(app): with app.app_context(): _db.create_all() yield _db _db.session.remove() _db.drop_all()

Parametrize

Run the same test with multiple inputs β€” eliminates duplicate test functions:

python
import pytest @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (-1, -2, -3), (0, 0, 0), (100, -50, 50), ]) def test_add(a, b, expected): assert add(a, b) == expected -- More complex parametrize with IDs @pytest.mark.parametrize("email,valid", [ ("alice@example.com", True), ("bob.smith@co.uk", True), ("not-an-email", False), ("missing@", False), ("@nodomain.com", False), ], ids=["valid_simple", "valid_subdomain", "no_at", "no_domain", "no_local"]) def test_email_validation(email, valid): assert is_valid_email(email) == valid -- Nested parametrize (cartesian product) @pytest.mark.parametrize("role", ["admin", "editor", "viewer"]) @pytest.mark.parametrize("method", ["GET", "POST", "DELETE"]) def test_permissions(role, method): -- Tests all 9 combinations result = check_permission(role, method) assert isinstance(result, bool)

Mocking

unittest.mock (built-in)

python
from unittest.mock import Mock, MagicMock, patch, AsyncMock def test_send_email_on_registration(mock_email_service): -- Create a mock email_service = Mock() email_service.send.return_value = {"status": "sent"} user_service = UserService(email_service=email_service) user_service.register("alice@example.com", "password123") -- Verify the mock was called correctly email_service.send.assert_called_once() call_args = email_service.send.call_args assert call_args.kwargs["to"] == "alice@example.com" assert "Welcome" in call_args.kwargs["subject"] -- patch as decorator: replaces the target for the duration of the test @patch("myapp.services.user_service.send_email") def test_registration_sends_email(mock_send_email): mock_send_email.return_value = None register_user("bob@example.com", "pass123") mock_send_email.assert_called_once_with( to="bob@example.com", subject="Welcome to our platform!" ) -- patch as context manager def test_api_call(): with patch("myapp.services.requests.get") as mock_get: mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"users": []} result = fetch_users() assert result == []

pytest-mock (cleaner syntax)

python
-- pytest-mock provides the mocker fixture def test_send_notification(mocker): mock_send = mocker.patch("myapp.notifications.send_sms") mock_send.return_value = True send_order_confirmation(order_id=42, phone="+1234567890") mock_send.assert_called_once_with( phone="+1234567890", message=mocker.ANY -- don't care about exact message content ) -- Mock a class method def test_payment_processing(mocker): mock_charge = mocker.patch.object(StripeClient, "charge") mock_charge.return_value = {"id": "ch_123", "status": "succeeded"} result = process_payment(amount=9999, currency="usd") assert result["success"] is True mock_charge.assert_called_once_with(amount=9999, currency="usd")

Testing Async Code

python
import pytest import asyncio -- Mark test as async @pytest.mark.asyncio async def test_async_fetch(): result = await fetch_user_async(user_id=1) assert result["id"] == 1 -- Async fixtures @pytest.fixture async def async_client(): async with AsyncClient(app=app, base_url="http://test") as client: yield client @pytest.mark.asyncio async def test_create_user_api(async_client): response = await async_client.post("/users", json={ "username": "testuser", "email": "test@example.com", }) assert response.status_code == 201 -- Mock async functions @pytest.mark.asyncio async def test_async_email(mocker): mock_send = mocker.AsyncMock(return_value={"status": "sent"}) mocker.patch("myapp.email.send_async", mock_send) await register_user_async("alice@example.com") mock_send.assert_awaited_once()

Test Organization and Best Practices

code
tests/ β”œβ”€β”€ conftest.py -- session/module fixtures β”œβ”€β”€ unit/ β”‚ β”œβ”€β”€ test_models.py β”‚ β”œβ”€β”€ test_services.py β”‚ └── test_utils.py β”œβ”€β”€ integration/ β”‚ β”œβ”€β”€ test_api.py β”‚ └── test_database.py └── e2e/ └── test_flows.py

Markers

python
-- conftest.py def pytest_configure(config): config.addinivalue_line("markers", "slow: marks tests as slow") config.addinivalue_line("markers", "integration: marks integration tests") -- Use markers on tests @pytest.mark.slow def test_large_data_processing(): ... @pytest.mark.integration def test_database_write(): ... -- Run only specific markers -- pytest -m "not slow" -- pytest -m "integration"

AAA Pattern

python
def test_order_total_with_discount(): -- Arrange cart = Cart() cart.add_item(Product(name="Widget", price=10.00), quantity=3) coupon = Coupon(code="SAVE20", discount_pct=20) -- Act total = cart.calculate_total(coupon=coupon) -- Assert assert total == 24.00 -- 30.00 - 20%

Common Interview Questions

Q: What is the difference between a mock and a stub?

A stub provides canned responses to calls made during the test β€” it replaces a dependency with a simplified version that returns predetermined data. A mock also records calls made to it and allows assertions about how it was called (which methods, with what arguments, how many times). In Python's unittest.mock, Mock objects are mocks (they also work as stubs). Most testing uses the terms interchangeably.

Q: What fixture scope would you use for a database connection and why?

session scope if the tests only read from the database (one connection shared across all tests β€” fastest). function scope if each test writes to the database β€” each test gets a fresh state, preventing test interdependence. A common pattern: session-scoped connection, function-scoped transaction that is rolled back after each test without committing, preserving the empty state.

Q: What is the difference between @patch and mocker.patch?

@patch is from unittest.mock and works as a decorator or context manager. mocker.patch is from pytest-mock and integrates with pytest's fixture system β€” it automatically undoes patches at the end of the test without needing a context manager or decorator. mocker.patch is generally cleaner in pytest tests.

Practice Python on Froquiz

Testing knowledge is increasingly expected in Python developer interviews. Test your Python skills on Froquiz β€” covering OOP, async, decorators, and core Python concepts.

Summary

  • pytest discovers tests by naming convention: files test_*.py, functions test_*
  • Fixtures provide setup/teardown β€” inject by name, use yield to separate setup from teardown
  • Fixture scope: function (default), class, module, session β€” wider scope = less setup overhead
  • conftest.py holds shared fixtures available to all tests in the directory
  • @pytest.mark.parametrize runs one test function with multiple input sets
  • unittest.mock.patch and pytest-mock's mocker.patch replace real dependencies with controllable fakes
  • Use @pytest.mark.asyncio for async tests; mocker.AsyncMock for async function mocks
  • Follow AAA (Arrange, Act, Assert) β€” one concept per test, descriptive test names

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • Modern JavaScript: ES2020 to ES2024 Features You Should Be UsingMar 17
  • Software Architecture Patterns: MVC, Clean Architecture, Hexagonal and Event-DrivenMar 17
  • PostgreSQL Full-Text Search: tsvector, tsquery, Ranking and Multilingual SearchMar 17
All Blogs