Courses/Python/Testing with pytest: Fixtures, Parametrisation & Mocks

    Lesson 34 • Advanced

    Testing with pytest: Fixtures, Parametrisation & Mocks

    Pytest is the standard testing framework for modern Python. Master professional testing strategies used by real engineering teams.

    What You'll Master

    This lesson takes you from basic testing → professional test suite architecture.

    ✔ Why pytest vs unittest

    ✔ Fixtures for reusable setup/teardown

    ✔ Fixture scopes and dependencies

    ✔ Parametrised tests for multiple inputs

    ✔ Mocking external systems (APIs, DBs, time)

    ✔ Integration testing strategies

    ✔ Professional test suite structure

    Part 1: Core pytest — Fixtures, Parametrisation & Basic Mocking

    🔥 1. Why pytest?

    pytest gives you:

    Featurepytestunittest
    Assertion syntaxassert x == yself.assertEqual(x, y)
    BoilerplateNone — just functionsClasses required
    FixturesPowerful, injectablesetUp/tearDown only
    ParametrisationBuilt-in decoratorManual loops
    Plugin ecosystemHuge (1000+ plugins)Limited

    Basic Test Example

    Try it Yourself »
    Python
    # test_math_utils.py
    
    def add(a, b):
        return a + b
    
    def test_add_two_numbers():
        assert add(2, 3) == 5
        print("✓ test_add_two_numbers passed!")
    
    # Run the test
    test_add_two_numbers()

    Run: pytest

    🧪 2. Basic Test Structure

    Naming rules (by convention):

    • Files: test_*.py
    • Functions: test_*
    • Classes: class TestSomething: (no __init__)

    Test Structure

    Try it Yourself »
    Python
    # test_strings.py
    
    def to_upper(text: str) -> str:
        return text.upper()
    
    def test_to_upper_basic():
        assert to_upper("hello") == "HELLO"
        print("✓ test_to_upper_basic passed!")
    
    def test_to_upper_already_upper():
        assert to_upper("WORLD") == "WORLD"
        print("✓ test_to_upper_already_upper passed!")
    
    # Run tests
    test_to_upper_basic()
    test_to_upper_already_upper()

    ⚙️ 3. What Is a Fixture?

    A fixture is reusable setup logic that you can "inject" into tests via function arguments.

    Fixture Example

    Try it Yourself »
    Python
    # Simulating pytest fixtures
    
    def sample_user():
        """Fixture that provides a sample user"""
        return {"id": 1, "name": "Brayan", "role": "admin"}
    
    def test_user_has_name():
        user = sample_user()
        assert user["name"] == "Brayan"
        print("✓ test_user_has_name passed!")
    
    def test_user_is_admin():
        user = sample_user()
        assert user["role"] == "admin"
        print("✓ test_user_is_admin passed!")
    
    test_user_has_name()
    test_user_is_admin()

    🧱 4. Fixture Scopes

    By default, fixtures are function-scoped. You can control lifespan with scope:

    ScopeRunsUse Case
    functionOnce per testSafest, most isolated (default)
    classOnce per classRelated tests share instance
    moduleOnce per fileExpensive setup (DB connections)
    sessionOnce per test runGlobal resources (app config)

    Fixture Scopes

    Try it Yourself »
    Python
    print("Fixture Scopes in pytest:")
    print("=" * 40)
    print("""
    @pytest.fixture(scope="function")  # default - fresh per test
    def db_connection():
        ...
    
    @pytest.fixture(scope="class")     # once per test class
    def api_client():
        ...
    
    @pytest.fixture(scope="module")    # once per file
    def config():
        ...
    
    @pytest.fixture(scope="session")   # once per entire test run
    def app_env():
        ...
    """)
    
    print("When to use which?")
    print("-" * 30)
    print("function → safest; isolated; most common")
    prin
    ...

    🔁 5. Setup & Teardown with yield

    Yield Fixtures

    Try it Yourself »
    Python
    import tempfile
    import os
    
    def temp_file_fixture():
        """Fixture with setup and teardown"""
        # Setup
        fd, path = tempfile.mkstemp(suffix=".txt")
        os.write(fd, b"initial content")
        os.close(fd)
        print(f"Setup: Created temp file {path}")
        
        # Return value to test
        yield path
        
        # Teardown
        os.unlink(path)
        print(f"Teardown: Deleted temp file")
    
    def test_temp_file():
        # Use generator to simulate fixture
        gen = temp_file_fixture()
        file_path = next(gen)
    ...

    🧪 6. Parametrised Tests

    Instead of writing multiple duplicate tests, use parametrisation:

    Parametrised Tests

    Try it Yourself »
    Python
    # Simulating @pytest.mark.parametrize
    
    def add(a, b):
        return a + b
    
    test_cases = [
        (1, 2, 3),
        (10, 5, 15),
        (-1, 1, 0),
        (0, 0, 0),
    ]
    
    def test_add_parametrized():
        for a, b, expected in test_cases:
            result = add(a, b)
            assert result == expected, f"Failed: add({a}, {b}) = {result}, expected {expected}"
            print(f"✓ add({a}, {b}) = {expected}")
    
    test_add_parametrized()
    print("\nAll parametrised tests passed!")

    🧪 7. Basic Mocking

    Basic Mocking

    Try it Yourself »
    Python
    from unittest.mock import Mock
    
    def fetch_user(api):
        """Function that uses an API"""
        return api.get("/user")
    
    def test_fetch_user():
        # Create a mock API
        fake_api = Mock()
        fake_api.get.return_value = {"id": 1, "name": "Boopie"}
    
        # Call function with mock
        result = fetch_user(fake_api)
        
        # Assertions
        assert result["name"] == "Boopie"
        fake_api.get.assert_called_once_with("/user")
        print("✓ test_fetch_user passed!")
    
    test_fetch_user()

    Part 2: Advanced Fixtures & Mocking

    🔧 Factory Fixtures

    Factory Fixtures

    Try it Yourself »
    Python
    def make_user(name, role="viewer"):
        """Factory function for creating users"""
        return {"name": name, "role": role}
    
    def test_user_factory():
        u1 = make_user("Alice")
        u2 = make_user("Bob", role="admin")
        
        assert u1["role"] == "viewer"
        assert u2["role"] == "admin"
        print(f"✓ Created users: {u1}, {u2}")
    
    test_user_factory()

    🧠 Combining Fixtures + Parametrisation

    Combined Example

    Try it Yourself »
    Python
    # Simulating combined fixtures and parametrisation
    
    numbers = [1, 2, 3]
    factors = [10, 20]
    
    def test_multiplied():
        results = []
        for number in numbers:
            for factor in factors:
                result = number * factor
                results.append((number, factor, result))
                print(f"✓ {number} × {factor} = {result}")
        
        print(f"\nTotal combinations: {len(results)}")
    
    test_multiplied()

    🔐 Monkeypatching

    Monkeypatching

    Try it Yourself »
    Python
    # Original function
    def get_price():
        return 999  # Imagine this calls an API
    
    # Test with monkeypatch simulation
    original_get_price = get_price
    
    def test_price():
        global get_price
        # Monkeypatch
        get_price = lambda: 10
        
        assert get_price() == 10
        print("✓ Monkeypatched get_price() returns 10")
        
        # Restore
        get_price = original_get_price
    
    test_price()
    print(f"Original get_price(): {get_price()}")

    🛰 Mocking External APIs

    Python
    from unittest.mock import Mock
    
    def fetch_user_data(client):
        response = client.get("https://api.example.com/user")
        return response.json()
    
    def test_fetch_user_with_mock():
        # Create mock client
        mock_client = Mock()
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "Boopie"}
        mock_client.get.return_value = mock_response
        
        # Test
        result = fetch_user_data(mock_client)
        
        assert result["name"] == "Boopie"
        mock_client.get.assert_
    ...

    🧪 Spy Objects

    Python
    from unittest.mock import Mock
    
    def test_call_tracking():
        # Create a mock that tracks calls
        func = Mock()
        
        # Make some calls
        func("hello")
        func("world", count=5)
        
        # Verify calls
        func.assert_called()
        assert func.call_count == 2
        
        # Check specific calls
        func.assert_any_call("hello")
        func.assert_any_call("world", count=5)
        
        print(f"✓ Function called {func.call_count} times")
        print(f"✓ Call history: {func.call_args_list}")
    
    test_call_
    ...

    Part 3: Professional Test Suite Architecture

    🧪 Test Suite Structure

    Professional Structure

    Try it Yourself »
    Python
    print("""
    Professional Test Suite Structure:
    ===================================
    
    project/
        app/
            core/
            api/
            models/
        tests/
            unit/           → pure logic, fast tests
            integration/    → DB, API, filesystem
            e2e/            → browser/full system
            fixtures/       → large reusable mocks
            conftest.py     → global fixtures
    
    Key principles:
    ---------------
    ✓ Separate unit, integration, and e2e tests
    ✓ Use conftest.py for shared fixtures
    ✓
    ...

    🧱 Integration Testing

    Integration Testing

    Try it Yourself »
    Python
    import sqlite3
    
    def test_database_integration():
        # Setup: Create in-memory database
        conn = sqlite3.connect(":memory:")
        cur = conn.cursor()
        
        # Create table
        cur.execute("CREATE TABLE users (id INT, name TEXT)")
        
        # Insert data
        cur.execute("INSERT INTO users VALUES (1, 'Alice')")
        conn.commit()
        
        # Query and verify
        result = cur.execute("SELECT name FROM users WHERE id=1").fetchone()
        assert result[0] == "Alice"
        
        # Cleanup
        conn.close()
    
    ...

    🧱 Mocking Time and Randomness

    Mocking Time

    Try it Yourself »
    Python
    import time
    import random
    
    # Save originals
    original_time = time.time
    original_randint = random.randint
    
    def test_mocked_time():
        # Mock time
        time.time = lambda: 1000.0
        assert time.time() == 1000.0
        print(f"✓ Mocked time: {time.time()}")
        
        # Restore
        time.time = original_time
    
    def test_mocked_random():
        # Mock random
        random.randint = lambda a, b: 42
        assert random.randint(1, 100) == 42
        print(f"✓ Mocked random: {random.randint(1, 100)}")
        
        # Restore
      
    ...

    🎓 Summary

    You've learned professional pytest strategies:

    ✅ Plain assert-based tests

    ✅ Fixtures for reusable setup/teardown

    ✅ Fixture scopes and dependencies

    ✅ Parametrised tests for multiple inputs

    ✅ Mocking external systems

    ✅ Integration testing patterns

    ✅ Professional test suite structure

    📋 Quick Reference — pytest

    SyntaxWhat it does
    def test_fn():Define a test (must start with test_)
    @pytest.fixtureReusable setup/teardown function
    @pytest.mark.parametrizeRun test with multiple inputs
    pytest.raises(ValueError)Assert an exception is raised
    mocker.patch('module.fn')Mock a function with pytest-mock

    🎉 Great work! You've completed this lesson.

    You can now write fixtures, parametrised tests, and mocks — the full professional pytest toolkit used at tech companies worldwide.

    Up next: CLI Tools — build polished command-line applications with argparse and Typer.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service