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
- pytest: a Python testing framework that lets you express assertions with the built-in
assertkeyword - Fixture: a reusable setup function that prepares shared data or state before tests run
- Mocking: swapping slow or unsafe dependencies for fake objects so tests stay fast
- 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
uvenvironment, code that callsrequests- 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.fixturethat 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.parametrizewith coverage flags to add more inputs and inspect the report. - Debug: break a
monkeypatchto 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.
💬 댓글
이 글에 대한 의견을 남겨주세요