[Python Series 11] Automate Testing with Pytest

한국어 버전

What is pytest?

Once your dependencies and environments are tidy, the next guardrail is automated testing. pytest has become the de facto standard because its syntax is simple yet extensible. We will move gradually: create a test file, extract fixtures, introduce mocking, and finish with coverage.

Key terms

  1. pytest: a Python testing framework that lets you express assertions with the built-in assert keyword
  2. Fixture: a reusable setup function that prepares shared data or state before tests run
  3. Mocking: swapping slow or unsafe dependencies for fake objects so tests stay fast
  4. Coverage: a metric that reports how many lines of your code ran during tests

Core ideas

Study notes

  • Time: 60–70 minutes
  • Prereqs: functions, classes, a uv environment, code that calls requests
  • Goal: run pytest end to end with fixtures, mocking, and coverage in one session
  • Tests are automated proofs for small units of behavior.
  • Fixtures prepare common data or environment before each test.
  • Mocking replaces external APIs, files, or other slow dependencies with controllable doubles.
  • Coverage tells you what lines remained untested.
  • Sections tagged "Optional" can wait; clear the Core path first.

Code walkthrough

Install and run (Core)

uv add --dev pytest pytest-cov
uv run pytest

Create a tests/ directory in the project root and add files named test_*.py; pytest discovers them automatically.

First assertion (Core)

# tests/test_app.py
from mealbot.meal import Meal


def test_to_markdown_formats_bullets():
    meal = Meal(menu=["돈까스", "샐러드"])
    assert meal.to_markdown() == "- 돈까스\n- 샐러드"

The plain assert statement compares expected values and shows a detailed diff on failure.

Extract shared prep with fixtures (Core)

Reducing repeated setup keeps tests short. Start with a fixture that returns a simple dictionary.



@pytest.fixture
def menu_payload():
    return {"menu": ["비빔밥", "미역국"], "calories": 640}


def test_from_json(menu_payload):
    meal = Meal.from_json(json.dumps(menu_payload, ensure_ascii=False))
    assert meal.calories == 640

Fixtures are injected by parameter name, so you get reuse and readability without extra ceremony.

🧪 What is a fixture? A function marked with @pytest.fixture that prepares data or state before tests. Any test parameter with the same name receives the prepared value.

Mock external APIs (Core → Plus)

Mocking lets you imitate unreliable APIs so tests stay deterministic. Think "return a fake response instead of firing a real request."

from unittest.mock import Mock


def test_fetch_meal_calls_request(monkeypatch):
    mock_response = Mock()
    mock_response.json.return_value = {"menu": ["카레"]}
    mock_response.raise_for_status.return_value = None

    def fake_get(url, timeout):
        assert "api" in url
        return mock_response

    monkeypatch.setattr("mealbot.fetcher.requests.get", fake_get)

    data = fetch_meal("demo-school")
    assert data["menu"] == ["카레"]

The built-in monkeypatch fixture swaps attributes at runtime, blocking real network calls. Use the same pattern for environment variables or other globals.

🔧 monkeypatch in practice Temporarily overrides module attributes or environment variables during a test. pytest restores the original state afterward.

Parameterize scenarios (Optional)

@pytest.mark.parametrize shines when one function must withstand multiple inputs. Skip now if your suite is still small.



@pytest.mark.parametrize(
    "calories,expected",
    [
        (0, "정보 없음"),
        (800, "800 kcal"),
    ],
)
def test_calorie_label(calories, expected):
    assert format_calorie_label(calories) == expected

One function covers many inputs, reducing blind spots.

Coverage and CI wiring (Optional)

Coverage is a late-stage bonus. It highlights untouched branches when you crave visual confirmation.

uv run pytest --cov=mealbot --cov-report=term-missing

term-missing lists untested lines. In GitHub Actions, upload the pytest --cov result so pull requests receive automatic comments.

Why it matters

Mastering the pytest flow gives you a safety net before refactors or deploys. Fixtures and mocking let you pin external APIs, file systems, and environment variables to predictable behavior. Coverage reports expose invisible gaps at a glance.

Practice

  • Follow along: run uv run pytest, capture the first diff when a test fails, and note how pytest explains the mismatch.
  • Extend: combine @pytest.mark.parametrize with coverage flags to add more inputs and inspect the report.
  • Debug: break a monkeypatch to force a failure, then fix the fixture and jot down the root cause.
  • Definition of done: you have run fixtures, mocking, and coverage commands back-to-back and confirmed both green checks and the coverage summary.

Wrap-up

Tests prove functionality in small slices and boost confidence before shipping. Next time we will bundle your new tools into a CLI automation app so the following advanced language topics have a real project to hook into.

💬 댓글

이 글에 대한 의견을 남겨주세요