Unit Testing
Write tests that fail for the right reason: unittest and pytest, fixtures, parametrization, test doubles, coverage, property-based testing and the red-green-refactor cycle
A payment service had ninety tests and a green build. On a Friday evening a customer was charged twice. The tests had mocked the payment gateway, so they proved that the service called a function with the right arguments. They never proved that the retry wrapper and the idempotency key worked together. The suite was fast, readable and confident about the wrong thing.
A test is a small experiment with a controlled input and a checked result. It does not prove that the system is correct. It proves that one behaviour held for one set of conditions, and it keeps proving it after the next refactor. The value comes from choosing behaviours that matter and from failing for the right reason when they break.
This lesson covers the standard library unittest module, the pytest workflow,
fixtures, parametrization, test doubles, coverage and the red-green-refactor
cycle. It ends with two case studies that show where unit tests stop and
integration tests begin.
Key terms
- A fast test that exercises one function or class in isolation from the network, the filesystem and other services.
- A test that exercises several components together, often with a real database, filesystem or HTTP boundary.
- A reusable setup and cleanup function that pytest injects into a test by name.
- A stand in for a real dependency, such as a stub, a fake, a spy or a mock.
- A test double that records how it was called and lets the test assert on those interactions.
- A working implementation with a simplified backend, such as an in memory repository instead of a database.
- Running the same test body with several input and expected value pairs.
- A test that states a property and lets a generator produce many inputs that should satisfy it.
- A measurement of which lines or branches a test suite executed, not a measurement of whether the assertions were meaningful.
- A technique that changes the source in small ways and checks whether the test suite notices, which probes assertion strength.
- A test that passes and fails without a code change, usually because it depends on time, randomness, network access or shared state.
- The test driven cycle of writing a failing test, making it pass with the smallest change, then improving the design without changing behaviour.
Unit test
Integration test
Fixture
Test double
Mock
Fake
Parametrization
Property-based test
Coverage
Mutation testing
Flaky test
Red-green-refactor
1. What a Test Proves
A test has three parts: arrange the inputs, act on the code, and assert the result. The pattern is called Arrange, Act, Assert, and it keeps the test readable when the setup grows. The assertion is the part that makes the test a test; running code without checking a result only proves that the code did not raise.
Tests are usually grouped by how much of the system they exercise. A unit test calls one function or class and replaces the slow or unpredictable dependencies. An integration test uses a real database, a real file or a real HTTP handler. An end to end test drives the whole system through its public surface. The pyramid below is a common default, not a law: the right mix depends on the failure modes that matter.
Description
A test pyramid with three layers. The wide bottom layer holds many fast unit tests that isolate one function or class. The middle layer holds fewer integration tests that use a real database or file. The narrow top layer holds a few end to end tests that drive the whole system through its public interface.
The pyramid is about confidence per second, not about counting tests. A unit test that checks a pure function runs in microseconds and points at the exact line that broke. An integration test catches a mismatch between two components that no unit test can see. An end to end test catches a wiring mistake that only appears when the whole system runs. Each layer answers a different question, and a suite with only one layer leaves a gap.
A good test has three properties. It is deterministic, so the same code produces the same result on every run. It is isolated, so it does not depend on the order of other tests or on state left behind by them. It fails for one reason, so the failure message points at the behaviour that changed rather than at a tangle of unrelated setup.
A test that has never failed is not evidence. Break the code on purpose, change a boundary value, or remove a branch, and confirm that the test notices. A suite that stays green after a deliberate bug is a suite of assertions that do not assert anything useful.
The pyramid also explains the common failure in the opening story. The unit tests mocked the payment gateway, so they checked the call shape but not the retry and idempotency interaction. A small integration test with a fake gateway that records requests would have caught the double charge. The next sections build from the smallest unit test up to that fake.
2. unittest: The Standard Library Baseline
unittest ships with Python, so a project can have tests before it has any
dependency. A test case is a class that inherits from unittest.TestCase, and
each method whose name starts with test is discovered and run. Assertions are
methods on the case, which gives a specific failure message instead of a bare
AssertionError.
import unittest
def apply_discount(price: float, percent: float) -> float:
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return round(price * (1 - percent / 100), 2)
class TestApplyDiscount(unittest.TestCase):
def test_twenty_percent(self):
self.assertEqual(apply_discount(100.0, 20.0), 80.0)
def test_zero_percent(self):
self.assertEqual(apply_discount(19.99, 0.0), 19.99)
def test_rejects_negative_percent(self):
with self.assertRaises(ValueError):
apply_discount(100.0, -1.0)
if __name__ == "__main__":
unittest.main()The test class is plain Python, so it can use helper methods and subclassing.
The three tests cover a normal case, a boundary case and an error case. That is
a reasonable first slice for one function. The error test uses the context
manager form of assertRaises, which keeps the failing call inside the block
and allows a second assertion on the exception if needed.
python -m unittest discover -s tests -v
python -m unittest tests.test_pricing.TestApplyDiscountDiscovery starts at the given directory, imports files that match test*.py
and runs the cases it finds. The dotted form runs one class or one method,
which is faster during a debugging session. The -v flag prints each test name
and its result, which is useful when a test order or a fixture leak is
suspected.
For repeated setup, setUp runs before every test and tearDown runs after it.
setUpClass and tearDownClass run once per class and must be class methods.
The per test form is the safe default because it prevents state from leaking
between tests.
import unittest
class TestCart(unittest.TestCase):
def setUp(self):
self.items = []
def tearDown(self):
self.items.clear()
def test_adds_an_item(self):
self.items.append("book")
self.assertEqual(self.items, ["book"])
def test_starts_empty(self):
self.assertEqual(self.items, [])The second test passes only because setUp creates a fresh list. Without it,
the order of the tests would decide the result, which is the definition of a
fragile suite. unittest also supports subTest, which keeps a loop of related
cases running and reports which value failed instead of stopping at the first
one.
Prefer a plain function and a few assertions over a deep class hierarchy.
Inheritance is useful when a family of tests shares expensive setup, but it
hides the arrange step and makes a failure harder to read. pytest can run
unittest cases, so a gradual migration does not require a rewrite.
3. pytest: Plain Assertions and Better Failure Messages
pytest runs plain functions that start with test_ and contain ordinary
assert statements. It rewrites the assertion bytecode to show the values that
produced the failure, so a bare assert produces a readable report without a
method for every comparison. The framework also discovers unittest cases, so a
project can use both without a migration step.
import pytest
def test_total_is_sum_of_lines():
lines = [{"total": 10.0}, {"total": 3.5}]
assert sum(line["total"] for line in lines) == 13.5
def test_rejects_negative_quantity():
with pytest.raises(ValueError, match="quantity must be positive"):
parse_quantity("-1")The second test checks both the exception type and the message fragment. The
match argument is a regular expression, so punctuation needs escaping when it
has a special meaning. Matching the message is valuable when the same exception
type covers several failure modes; matching it too tightly makes the test fail
on a harmless wording change.
Floating point comparisons need a tolerance. pytest.approx compares within a
relative or absolute tolerance and produces a useful diff when the values are
far apart.
import pytest
def test_average_is_close():
assert (0.1 + 0.2) == pytest.approx(0.3)
assert [0.1 + 0.2, 1.0] == pytest.approx([0.3, 1.0])The command line is part of the workflow. -q quiets the header, -k selects
tests by name expression, -x stops at the first failure, --lf runs only the
last failures and -vv prints a longer diff. python -m pytest makes the
interpreter explicit and avoids a pytest executable that belongs to another
environment.
python -m pytest -q
python -m pytest -k "discount and not slow"
python -m pytest -x --lf
python -m pytest tests/test_pricing.py::test_twenty_percent -vvConfiguration belongs in pyproject.toml so every developer and CI job uses
the same defaults. --strict-markers turns an unregistered marker into an
error, and --strict-config catches a misspelled option instead of ignoring it.
[tool.pytest.ini_options]
minversion = "9.0"
addopts = "-ra --strict-markers --strict-config"
testpaths = ["tests"]
markers = [
"slow: tests that take more than a second",
"integration: tests that need a real service",
]The video below is a long, practical introduction to the same workflow. It is useful for seeing fixtures, parametrization and mocking in one session, and it can be watched in sections rather than end to end.
Description
A full video course on testing Python code with pytest. It starts with a first test, moves through class based tests, fixtures, markers, parametrization and mocking, and shows the failure output at each step.
The fill in the blank exercise checks the commands that select and report tests.
How to answer
Fill-in-the-blank exercise with 3 blanks. Type the missing words, then check your answer.
Complete the pytest command line.
Use python -m blank to run the suite, add blank to stop at the first failure, and use blank to run only the tests that failed in the previous run.
Word Bank, drag words into the blanks above:
Tests rely on assert, and Python removes assert statements when it runs with
-O. Do not run the test suite with optimization enabled, and do not use
assert for input validation in production code. The test runner is the one
place where a bare assert is the right tool.
4. Fixtures: Setup, Teardown and Scope
A fixture is a function that prepares a resource and returns it. pytest injects the fixture by name when a test declares it as an argument. A fixture can also yield, in which case the code after the yield runs as teardown even when the test fails.
import sqlite3
import pytest
@pytest.fixture
def connection():
conn = sqlite3.connect(":memory:")
conn.execute("create table orders (id integer primary key, total real)")
yield conn
conn.close()
def test_inserts_an_order(connection):
connection.execute("insert into orders (total) values (?)", (10.5,))
count = connection.execute("select count(*) from orders").fetchone()[0]
assert count == 1The yield splits setup from teardown without a separate function. If the test
raises, pytest still resumes the generator and runs the cleanup. That is the
same guarantee as a with block, expressed as a fixture that several tests can
share.
The built in tmp_path fixture provides a fresh directory per test. It is the
right place for files, SQLite databases and generated reports, because it is
isolated from the repository and cleaned up by pytest.
from pathlib import Path
def test_writes_a_report(tmp_path: Path):
report = tmp_path / "summary.txt"
report.write_text("total=13.5\n", encoding="utf-8")
assert report.read_text(encoding="utf-8") == "total=13.5\n"Fixture scope controls how often a fixture is created. The default is function scope, which gives every test a fresh resource. Class, module, package and session scopes are available for expensive resources that are safe to share. The order is deliberate: higher scoped fixtures are set up first, and teardown runs in reverse.
Description
A sequence diagram of fixture setup and teardown. pytest creates a session fixture first, then a function fixture, then runs the test. When the test finishes, the function fixture tears down before the session fixture, even if the test raised.
Fixtures defined in conftest.py are available to every test below that
directory without an import. That is convenient for a shared database factory
or a settings object, and dangerous when the file becomes a dump for unrelated
helpers. Keep the file small and move domain specific factories into a module
that tests import explicitly.
Prefer function scope until a profile shows that setup dominates the runtime. A session scoped fixture that mutates state is a common source of order dependent failures, because one test can leave the shared resource in a state that the next test does not expect.
5. Parametrization and Property-Based Testing
A parametrized test runs the same body with several inputs. It replaces a loop inside a test, which stops at the first failure and hides the failing case in a stack trace. pytest reports each parameter set as a separate test and can give each one a readable id.
import pytest
@pytest.mark.parametrize(
("price", "percent", "expected"),
[
(100.0, 20.0, 80.0),
(19.99, 0.0, 19.99),
(50.0, 100.0, 0.0),
pytest.param(10.0, 150.0, None, id="rejects-out-of-range"),
],
)
def test_discount(price, percent, expected):
if expected is None:
with pytest.raises(ValueError):
apply_discount(price, percent)
else:
assert apply_discount(price, percent) == expectedThe fourth case mixes an error case into the table, which is convenient for a small function and a little awkward for a large one. A separate test for the error path is often clearer. The point of parametrization is to make the examples visible, not to force every case into one table.
Property-based testing goes further. Instead of choosing examples, the test states a property and a generator produces many inputs that should satisfy it. Hypothesis shrinks a failing input to a small reproducible example and saves it as a regression case.
from hypothesis import given, strategies as st
def encode(value: str) -> bytes:
return value.encode("utf-8")
def decode(value: bytes) -> str:
return value.decode("utf-8")
@given(st.text())
def test_round_trip(value: str):
assert decode(encode(value)) == valueThe property is the round trip, and the generator covers empty strings, Unicode, combining characters and text that a hand written table would miss. Hypothesis is a dependency, so it belongs in the test group rather than the runtime dependencies. Its strategies compose, which means lists of integers, dictionaries with text keys and nested structures are all one expression away.
A property test does not replace example tests. Examples document the business rules, and properties explore the input space around them. Keep both, and keep the property small enough that a failure points at one behaviour.
6. Test Doubles and Mocking
A test double replaces a real dependency. The vocabulary matters because the tools behave differently. A dummy is passed but never used. A stub returns a fixed value. A spy records calls and can also return a value. A mock is a spy with assertions built in. A fake is a working implementation with a simplified backend, such as an in memory repository instead of a database.
unittest.mock provides Mock and MagicMock, and patch replaces an
attribute for the duration of a block or a test. autospec=True makes the mock
validate the signature of the real callable, which catches a wrong argument
before the assertion does.
from unittest.mock import Mock, patch
def fetch_user(client, user_id: int) -> dict:
return client.get_json(f"/users/{user_id}")
def test_fetch_user_builds_the_url():
client = Mock()
client.get_json.return_value = {"id": 7, "name": "Ada"}
assert fetch_user(client, 7)["name"] == "Ada"
client.get_json.assert_called_once_with("/users/7")The mock replaces the client, which is the boundary the function depends on. It
does not replace fetch_user itself, because then the test would assert that a
mock called a mock. Patch the thing under test only when the goal is to verify
that a side effect happened, and prefer a fake when the interaction is complex.
monkeypatch is the pytest fixture for temporary attribute and environment
changes. It restores the original value automatically, which is safer than a
manual save and restore.
import os
from app.config import read_mode
def test_reads_mode_from_environment(monkeypatch):
monkeypatch.setenv("APP_MODE", "production")
assert read_mode() == "production"
def test_can_remove_the_variable(monkeypatch):
monkeypatch.delenv("APP_MODE", raising=False)
assert read_mode() == "development"The matching exercise connects each test double to its behaviour.
How to answer
Matching exercise with 5 pairs. Match each item on the left with its answer on the right.
Match each test double to the behaviour it provides.
Drag answers from here:
Patch where the name is used, not where it was defined. If a module does
from payments import charge, patching payments.charge changes the original
module but leaves the imported name bound to the old function. Patch
billing.charge instead, because that is the lookup the code under test
performs.
7. Coverage, Mutation and What Tests Miss
Coverage measures which lines or branches the suite executed. Line coverage
counts a line when it runs, and branch coverage also requires each side of a
conditional to run. pytest-cov integrates coverage with pytest, and the
coverage command can produce terminal, HTML and XML reports.
python -m pytest --cov=analytics --cov-branch --cov-report=term-missing
python -m coverage report --show-missing
python -m coverage htmlThe term-missing report lists the lines that never ran. That list is a map of
untested code, and it is most useful when it points at a branch that handles an
error or an empty collection. A line that ran is not a line that was checked;
coverage cannot see whether the assertion would fail for a wrong value.
Mutation testing probes that gap. A mutation tool changes the source in small ways, such as flipping a comparison or replacing a constant, and runs the suite against each change. A test suite with strong assertions kills most mutants; a suite that only executes code leaves many mutants alive. The technique is slower than coverage and usually reserved for a critical module or a nightly job.
python -m pytest --cov=analytics --cov-branch
python -m pytest -q --cov=analytics --cov-report=jsonFlaky tests are the other source of false confidence. A test that depends on the current time, an unseeded random value, a live network call, the order of files on disk or a shared cache can pass on one machine and fail on another. The fix is to inject the dependency, not to add a retry. A retry hides the flakiness and turns a deterministic bug into an intermittent one.
A coverage percentage is not a quality score. A module can reach one hundred percent line coverage with no assertion that would fail for a wrong result. Use coverage to find unexecuted code, then read the tests and ask what would break if the implementation returned a plausible wrong value.
8. TDD and Test Design
Test driven development is a short cycle: write a failing test, make it pass with the smallest change, then refactor while the test stays green. The cycle is a design tool because it forces the interface to be usable before the implementation exists.
Description
A state diagram of the red-green-refactor cycle. A new behaviour starts in the red state with a failing test, moves to green when the smallest implementation passes, and then moves to refactor while the suite stays green. The next behaviour starts a new red state.
The order of the drag and drop exercise is the same cycle with the cleanup step made explicit.
How to answer
Ordering exercise with 5 items. Move them into the correct order, then check your answer.
Order the steps of one red-green-refactor cycle.
A useful test name describes the behaviour and the condition, for example
test_discount_rejects_percent_above_hundred. The body follows Arrange, Act,
Assert and usually contains one logical assertion. A test that checks five
unrelated things reports the first failure and hides the rest, which makes the
debugging session longer.
TDD is not a requirement for every change. A spike or an exploratory script may start without tests, and a legacy module may need characterization tests before it can be refactored. The useful part is the rhythm: a small failing example, a small pass, and a cleanup that keeps the suite green. The ritual without the rhythm produces tests that mirror the implementation instead of the behaviour.
Test behaviour through the public interface, not private attributes and internal call order. A test that breaks when a helper is renamed is testing the implementation, and it will slow down every refactor without catching a user visible bug.
9. Real-World Case Studies
The two cases below show a unit test with a real filesystem fixture and an integration style test with a fake boundary.
Case 1: Test a CSV summary with tmp_path
A reporting function reads a CSV file, sums a column and returns a dictionary.
The test writes a small file into tmp_path, calls the function and checks the
result. No production file is touched and no cleanup is required.
import csv
from pathlib import Path
def summarize(path: Path) -> dict[str, float]:
with path.open(newline="", encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
totals = [float(row["total"]) for row in rows]
return {"count": len(totals), "sum": sum(totals)}
def test_summarize_reads_all_rows(tmp_path: Path):
path = tmp_path / "orders.csv"
path.write_text("id,total\n1,10.5\n2,3.25\n", encoding="utf-8")
result = summarize(path)
assert result == {"count": 2, "sum": 13.75}The fixture is the filesystem, and tmp_path makes it deterministic. A
parametrized version can cover an empty file, a single row and a malformed
number without duplicating the setup.
import pytest
@pytest.mark.parametrize(
("content", "expected"),
[
("id,total\n", {"count": 0, "sum": 0}),
("id,total\n1,2.5\n", {"count": 1, "sum": 2.5}),
],
)
def test_summarize_edge_cases(tmp_path, content, expected):
path = tmp_path / "orders.csv"
path.write_text(content, encoding="utf-8")
assert summarize(path) == expectedThe empty file case is a boundary that a happy path test misses. The malformed
number case belongs in a separate test that expects ValueError, because the
assertion is about the error path rather than the summary shape.
Case 2: Test an API client with a fake transport
An API client wraps httpx and returns a parsed user. Testing it against the
real network would be slow, flaky and dependent on a third party. A fake
transport records the request and returns a prepared response, which keeps the
test fast and still exercises the client's parsing and error handling.
import httpx
import pytest
class FakeTransport(httpx.BaseTransport):
def __init__(self, status_code: int, payload: dict):
self.status_code = status_code
self.payload = payload
self.requests: list[httpx.Request] = []
def handle_request(self, request: httpx.Request) -> httpx.Response:
self.requests.append(request)
return httpx.Response(self.status_code, json=self.payload)
def fetch_user(client: httpx.Client, user_id: int) -> dict:
response = client.get(f"/users/{user_id}")
response.raise_for_status()
return response.json()
def test_fetch_user_parses_the_payload():
transport = FakeTransport(200, {"id": 7, "name": "Ada"})
client = httpx.Client(base_url="https://api.test", transport=transport)
assert fetch_user(client, 7)["name"] == "Ada"
assert transport.requests[0].url.path == "/users/7"
def test_fetch_user_raises_on_server_error():
transport = FakeTransport(500, {"error": "boom"})
client = httpx.Client(base_url="https://api.test", transport=transport)
with pytest.raises(httpx.HTTPStatusError):
fetch_user(client, 7)The fake is a real transport subclass, so the client's own request building and
response parsing run. That is the integration boundary that matters. Mocking
client.get directly would skip the URL construction and the status handling,
which are the parts most likely to break.
Choose the smallest real boundary that still exercises the behaviour. A fake HTTP transport is more faithful than a mock and faster than a live server. A mock is appropriate when the interaction itself is the behaviour being tested, such as confirming that an audit event was emitted.
10. Choosing a Test Level, Pitfalls and a Checklist
The right test level is the one that fails for the reason you care about. The table below is a starting point for the decision.
Level | Use it for | Replace | Speed |
|---|---|---|---|
Unit | Pure logic, parsing, validation, edge cases | Network, clock, randomness | Microseconds |
Integration | Database queries, file formats, HTTP handlers | The other side of the boundary | Milliseconds |
End to end | Critical user journeys and wiring | Nothing | Seconds to minutes |
A test suite matures in stages. The timeline below is not a schedule; it is a map of what usually gets added after the first few tests.
Description
A timeline of test suite maturity. The first stage has a few happy path tests, the second adds edge cases and error paths, the third adds fixtures and parametrization, the fourth adds coverage and mutation checks, and the final stage adds fast feedback in continuous integration.
The quadrant below places the three levels on speed and confidence. The useful suite fills the fast and narrow corner first, then adds the smallest number of slower tests that cover the boundaries.
Description
A quadrant chart of test levels by speed and confidence. The horizontal axis runs from slow to fast feedback, and the vertical axis runs from narrow confidence to broad confidence. Unit tests sit in the fast and narrow corner, integration tests sit near the middle, and end to end tests sit in the slow and broad corner.
The recurring mistakes are about what the test is allowed to know.
- Asserting on a private helper or an internal call order instead of observable behaviour.
- Mocking the function under test, which proves only that a mock called a mock.
- Patching a name in the defining module when the code imports it by value.
- Sharing a mutable fixture across tests and depending on execution order.
- Using
time.sleepor a live network call to make a test pass. - Chasing a coverage number while the assertions stay weak.
- Adding retries to a flaky test instead of removing the source of flakiness.
- Writing one giant test that checks several behaviours and hides the first failure behind later ones.
A short review before merging tests: does each test fail for one reason, does the name describe the behaviour, is the boundary real or intentionally faked, and would a plausible wrong implementation still pass?
2026 Updates (as of November 2026)
- Python 3.15.2 is the current stable release, and the examples target 3.11 or newer so they remain compatible with the supported branches.
- pytest 9.2.0 is the current release. The 9.x line requires Python 3.10 or newer and keeps the plain assert, fixture and parametrization model described here.
- Coverage.py 7.17.2 is the current release, with branch coverage and the
term-missingreport used in this lesson. - Hypothesis 6.170.15 is the current release, and property-based tests remain a practical complement to example tests for parsers, encoders and state machines.
pytestconfiguration inpyproject.tomlunder[tool.pytest.ini_options]is the recommended project level setup, and--strict-markersplus--strict-configcatch configuration drift early.unittestremains in the standard library with no dependency, which keeps a zero dependency project testable and gives pytest a compatible discovery target.
The tooling has not changed the fundamentals. A test still needs a controlled input, a checked result and a failure that points at one behaviour. New runners and coverage tools make that easier to observe; they do not make a weak assertion stronger.
π Did you know?
unittestwas inspired by the xUnit family of testing frameworks, which is why its class based style looks similar across languages.- pytest can collect and run
unittest.TestCaseclasses, so a project can migrate one test at a time instead of pausing feature work for a rewrite. pytest.approxcompares nested lists and dictionaries as well as scalar numbers, which makes floating point assertions in data tests much shorter.- A mutation test can survive because the test suite never asserted the changed value, which is exactly the gap that coverage cannot see.
Practice Questions
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A service has many unit tests that mock the payment gateway and a few integration tests that use a fake gateway. Which failure is the unit tests least likely to catch?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Why does the test assert (0.1 + 0.2) == pytest.approx(0.3) instead of using a plain equality assertion?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A function scoped fixture opens a connection, yields it to the test and then closes it. The test raises an AssertionError. What happens to the code after the yield?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A test uses pytest.raises(ValueError, match="quantity must be positive"). What does the match argument check?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A function calls client.get_json(f"/users/{user_id}"). A test replaces client with a Mock. Which assertion verifies the exact call?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A module does from app.config import read_mode, and a test wants to replace read_mode for one test. Where should the patch target be?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
What advantage does @pytest.mark.parametrize have over a for loop inside one test?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A module reaches one hundred percent line coverage but a mutation that flips a comparison is not caught. What does that combination suggest?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
In the red-green-refactor cycle, why must the first run of a new test fail?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A test writes a CSV file for a reporting function. Which pytest fixture gives an isolated directory that is cleaned up automatically?
Key Takeaways
- A test proves one behaviour for one set of conditions, and a good suite chooses the level that matches the failure mode.
unittestprovides a zero dependency baseline, while pytest adds plain assertions, fixtures, parametrization and a strong plugin ecosystem.- Fixtures separate setup from teardown, and yield fixtures clean up even when a test fails.
- Parametrization makes cases visible, and property-based testing explores the input space around them.
- Test doubles belong at the boundary; patching internals couples the test to the implementation.
- Coverage finds unexecuted code, mutation testing probes assertion strength, and neither replaces a careful reading of the tests.
- Red-green-refactor keeps the implementation small and the design usable.
References
Every factual claim links to its source, with the access date printed so the source can be rechecked.