Discovery and running tests — what pytest runs, and how to choose
Which files, functions and classes pytest treats as tests, importing your code from a src/ and tests/ layout, node IDs, and running exactly what you need with -k, -x, --lf, --ff and --co.
- 1Encounter
- 2Understand
- 3Worked
- 4Predict
- 5Apply
- 6Stretch
The problem we are solving
So far every test has lived in one file, next to the code it tests, and plain pytest has been enough. Real projects do not stay that small. The code moves into a package, the tests move into their own folder, and the very next run looks like this:
$ pytest -q
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_cart.py ______________________
ImportError while importing test module '/home/you/shop/tests/test_cart.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_cart.py:1: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR tests/test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01sNot one test ran. The code is fine; pytest simply could not find it.
Once that is fixed, a second problem arrives: there are now dozens of tests, one of them fails, and every rerun runs all of them again. You want to run only that one — or only the ones that failed last time.
Both problems come from the same place. Before pytest runs anything, it collects: it walks the folders, imports files, and builds a list of tests. This chapter is about how that list is built, and how to choose from it.
By the end of this chapter you can
- State the rules
pytestuses to decide what is a test - Lay out a project with
src/andtests/, and make the tests able to import the code - Group tests in classes, and read a node ID such as
tests/test_cart.py::TestCart::test_total_adds_tax - Run one file, one class, one test, or a
-kselection of them - Use
-x,--maxfail,--lf,--ff,--coand--durationsin daily work - Read
pytest's exit code and say what it means
Prerequisites: assert and failure reports.
Before you write the test
In this chapter the question before the first line of test code is not only what to test, but where the test will live and how `pytest` will find it. A test that is never collected is worse than no test: it looks like protection and is not.
1. Know the promise. The code we will test is a small shop package:
apply_discount(price, percent)returns the price reduced bypercent, rounded to 2 places. A percent outside 0–100 raisesValueError.add_tax(price, rate=0.15)returns the price with tax added, rounded to 2 places.Cartcollects(name, price, quantity)items;subtotal()multiplies by quantity;total()is the subtotal with tax.
2. Have the setup ready. A virtual environment with pytest installed; the code laid out in src/shop/; and — the part this chapter is about — a way for the tests to import shop. If that last part is missing, nothing else matters.
3. Plan the cases, and decide where each one lives. One test file per module (tests/test_pricing.py mirrors src/shop/pricing.py), and the cart tests grouped in one class. Writing the node ID into the plan forces you to decide that up front:
| Case | Input | Expected | Node ID | |---|---|---|---| | Normal discount | apply_discount(200.0, 10) | 180.0 | tests/test_pricing.py::test_discount_ten_percent | | Boundary: no discount | apply_discount(200.0, 0) | 200.0 | tests/test_pricing.py::test_discount_zero | | Default tax | add_tax(100.0) | 115.0 | tests/test_pricing.py::test_add_tax_default_rate | | Edge: empty cart | Cart().subtotal() | 0 | tests/test_cart.py::TestCart::test_empty_cart_subtotal | | Quantity counts | 3 pens at 15.0 | 45.0 | tests/test_cart.py::TestCart::test_subtotal_counts_quantity | | Total includes tax | 1 bag at 100.0 | 115.0 | tests/test_cart.py::TestCart::test_total_adds_tax | | Invalid percent | apply_discount(200.0, 150) | ValueError | later — testing exceptions has its own chapter |
4. Decide what not to test. Do not test round() or list.append — Python's own behaviour is not your promise. Do not test that Cart.items is a list of tuples; that is an internal detail you may change tomorrow. Test what a caller sees: subtotal() and total().
Now the chapter builds exactly that plan, and every problem along the way is a discovery problem.
The rules of discovery
pytest does not run whatever it finds. It follows four rules, and anything outside them is ignored without a word:
- Files named
test_*.pyor*_test.py - Functions in those files whose names start with
test - Classes whose names start with
Test— and which have no__init__method - Methods inside such a class whose names start with
test
Why rules at all, instead of "run every function"? Because your project is full of functions that are not tests — helpers, the code under test itself — and pytest needs a way to tell them apart without you registering anything.
The fastest way to believe the rules is to break them on purpose. test_rules.py:
def test_found():
assert True
def check_not_found():
assert False
class TestGroup:
def test_method_found(self):
assert True
def helper(self):
assert False
class CartTests:
def test_ignored(self):
assert False
class TestWithInit:
def __init__(self):
self.value = 1
def test_never_runs(self):
assert self.value == 1Next to it, rules_test.py holds one passing test_suffix_style, and helpers.py holds a test_in_helpers that does assert False. Now pytest -v:
collecting ... collected 3 items
rules_test.py::test_suffix_style PASSED [ 33%]
test_rules.py::test_found PASSED [ 66%]
test_rules.py::TestGroup::test_method_found PASSED [100%]
=============================== warnings summary ===============================
test_rules.py:22
/home/you/disc/test_rules.py:22: PytestCollectionWarning: cannot collect test class 'TestWithInit' because it has a __init__ constructor (from: test_rules.py)
class TestWithInit:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 3 passed, 1 warning in 0.01s =========================There are five assert False lines in that folder, and the run is green. What was skipped, and why:
check_not_found— the name does not start withtest.TestGroup.helper— inside a test class, but its own name does not start withtest. Helper methods are allowed; they are just not tests.CartTests— the class name has to start withTest.TestWithInit—pytestmust create the class itself, with no arguments, once per test. A class with its own__init__cannot be trusted to allow that, so it is skipped — with a warning, the only noise in the whole run.helpers.py— the file name matches neither pattern, so it is never even imported.
The silent part is the dangerous part. If you writedef tset_total():by mistake, the suite stays green forever. When a new test "passes" suspiciously easily, check that it was collected at all —--co, below, is how.
A real layout: src/ and tests/
shop/
├── pyproject.toml
├── src/
│ └── shop/
│ ├── __init__.py
│ ├── cart.py
│ └── pricing.py
└── tests/
├── test_cart.py
└── test_pricing.pyThe code lives in src/shop/; the tests live in tests/ and never ship with the package. This is the src layout. Its point is that the code cannot be imported "by accident" just because you happen to be standing in the project folder — so the tests import it the same way a real user would.
src/shop/pricing.py:
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError(f"percent must be between 0 and 100: {percent}")
return round(price * (100 - percent) / 100, 2)
def add_tax(price, rate=0.15):
return round(price * (1 + rate), 2)src/shop/cart.py:
from shop.pricing import add_tax
class Cart:
def __init__(self):
self.items = []
def add(self, name, price, quantity=1):
self.items.append((name, price, quantity))
def subtotal(self):
return sum(price * quantity for _, price, quantity in self.items)
def total(self):
return add_tax(self.subtotal())tests/test_pricing.py — the first three rows of the plan:
from shop.pricing import add_tax, apply_discount
def test_discount_ten_percent():
assert apply_discount(200.0, 10) == 180.0
def test_discount_zero():
assert apply_discount(200.0, 0) == 200.0
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0That from shop.pricing import ... is exactly what produced the ModuleNotFoundError at the top of the chapter.
Why shop cannot be found
import shop succeeds only if some folder on sys.path contains a shop folder. The package lives in src/, and src/ is not on sys.path. pytest adds the folder of each test file (here tests/) so a test can import its neighbours — but nothing adds src/.
The wrong way is to patch sys.path inside the test file:
import sys
sys.path.insert(0, "src")
from shop.cart import Cart
def test_empty_cart_subtotal():
assert Cart().subtotal() == 0From the project folder it passes. Then someone runs it from inside tests/:
$ cd tests
$ pytest -q
==================================== ERRORS ====================================
________________________ ERROR collecting test_cart.py _________________________
ImportError while importing test module '/home/you/shop/tests/test_cart.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
test_cart.py:5: in <module>
from shop.cart import Cart
E ModuleNotFoundError: No module named 'shop'
=========================== short test summary info ============================
ERROR test_cart.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.01s"src" is a path relative to wherever you happen to be, so the test now depends on the folder you type the command in. And every test file needs the same three lines. There are two right ways.
Right way 1: tell pytest where the code is. In pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]pythonpath adds src/ to sys.path before any test is imported — resolved from the project root, not from wherever you stand. testpaths says where to look when you type plain pytest. Now:
$ pytest
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/you/shop
configfile: pyproject.toml
testpaths: tests
collected 6 items
tests/test_cart.py ... [ 50%]
tests/test_pricing.py ... [100%]
============================== 6 passed in 0.01s ===============================configfile: pyproject.toml in the header is the proof that pytest read your settings. Configuration files get a chapter of their own later; these two lines are all you need now.
Right way 2: install the package. If pyproject.toml describes a real package (a [build-system] and a [project] table), run pip install -e . once. That installs the package in editable mode: import shop now works everywhere — tests, REPL, scripts — and edits to src/ are picked up without reinstalling. After it, pytest -q gives the same 6 passed with no pythonpath at all. Most packaged projects do this; pythonpath is the quicker choice when the project is not a package.
Grouping tests in classes
tests/test_cart.py puts the cart rows of the plan in one class:
from shop.cart import Cart
class TestCart:
def test_empty_cart_subtotal(self):
assert Cart().subtotal() == 0
def test_subtotal_counts_quantity(self):
cart = Cart()
cart.add("pen", 15.0, quantity=3)
assert cart.subtotal() == 45.0
def test_total_adds_tax(self):
cart = Cart()
cart.add("bag", 100.0)
assert cart.total() == 115.0$ pytest -q tests/test_cart.py
... [100%]
3 passed in 0.01sThere is no base class to inherit from — TestCart is a plain class, and self is just the required first parameter. Why bother? Because the class gives related tests one name, and that name becomes something you can select: run "all the cart tests" with one argument.
What the class is not for is sharing state. pytest creates a fresh instance for every test method, so a value stored on self in one test is gone in the next. That is deliberate — tests that depend on each other's leftovers pass or fail depending on the order they run in. Each test above builds its own Cart(). Fixtures, in a later chapter, are the proper tool for shared setup.
Node IDs: the address of a test
Ask pytest to list what it collected without running anything — --collect-only, or --co for short:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
6 tests collected in 0.01sCompare it with the plan's last column: every row is there, at the address you chose. Each line is a node ID — the file path, then ::, then the class if there is one, then :: and the test name. It is the same string that appears after FAILED in a failure summary, so you can copy it from a failure and paste it straight back into the command line.
Choosing what to run
By path or node ID
The wrong way to run one test is to comment out the others, or to rename them so pytest skips them — and then forget to undo it. The right way is to give pytest an address:
$ pytest -q tests/test_pricing.py
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart
... [100%]
3 passed in 0.01s
$ pytest -q tests/test_cart.py::TestCart::test_total_adds_tax
. [100%]
1 passed in 0.01sA folder, a file, a class, a single test — each level of the node ID narrows the selection, and the code is never touched.
By name: -k
-k takes an expression and keeps the tests whose names match it:
$ pytest -q -k discount
.. [100%]
2 passed, 4 deselected in 0.01sThe matching is a case-insensitive substring search, and words combine with and, or, not and parentheses. Quote the expression so the shell passes it as one argument:
$ pytest -q -k "discount or tax"
.... [100%]
4 passed, 2 deselected in 0.01s
$ pytest -q -k "TestCart and not tax"
.. [100%]
2 passed, 4 deselected in 0.01sThis is why test names are worth choosing with care: test_discount_zero can be found by discount, by zero, or by both. A name like test_2 can be found by nothing.
One detail catches everyone once. -k matches the test's name and the names above it — its class and its file:
$ pytest --co -q -k "not cart"
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
3/6 tests collected (3 deselected) in 0.01sNone of the three TestCart tests has cart in its own name, yet all three were dropped — they live in test_cart.py. When a -k selection surprises you, add --co and look before you run.
Selecting by a label you attach yourself, rather than by name, is what markers and -m are for, in chapter twelve.
When something fails
Break the code on purpose: change the default rate in add_tax from 0.15 to 0.18. Two tests now fail. (--tb=no hides the failure reports so the shape of the run is visible; you read those reports in the last chapter.)
$ pytest -q --tb=no
..F..F [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01s-x stops at the first failure. When one broken function makes ten tests fail, the first report is the one worth reading; the other nine are echoes:
$ pytest -q -x --tb=no
..F
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 2 passed in 0.01s--maxfail=N stops after N failures. -x is exactly --maxfail=1.
--lf (last failed) reruns only what failed last time. pytest remembers failures in a .pytest_cache folder in the project:
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 2 deselected in 0.01sOnly the two failures ran. Put the rate back, and --lf runs those two again and they pass. Run --lf once more with nothing left failing, and pytest prints run-last-failure: no previously failed tests, not deselecting items. and runs everything.
--ff (failed first) runs the failures first, then all the rest. The quick answer comes at the top, the full safety check after:
$ pytest -q --ff --tb=no
FF.... [100%]
=========================== short test summary info ============================
FAILED tests/test_cart.py::TestCart::test_total_adds_tax - assert 118.0 == 115.0
FAILED tests/test_pricing.py::test_add_tax_default_rate - assert 118.0 == 115.0
2 failed, 4 passed in 0.01sFF.... — the two failures ran first, before the four that passed.
A good loop while fixing a bug: pytest --lf -x until it is green, then plain pytest once. Why the last step? Because --lf only reruns what was red — it cannot tell you that your fix broke something that used to be green.
Output from your tests: capturing and -s
print inside a test seems to do nothing. (--tb=short below just makes the failure report shorter.) pytest captures everything a test writes to the screen and shows it only if that test fails:
def test_quiet():
print("subtotal is", 45.0)
assert 45.0 == 45.0
def test_loud():
print("subtotal is", 40.0)
assert 40.0 == 45.0$ pytest -q --tb=short
.F [100%]
=================================== FAILURES ===================================
__________________________________ test_loud ___________________________________
test_print.py:8: in test_loud
assert 40.0 == 45.0
E assert 40.0 == 45.0
----------------------------- Captured stdout call -----------------------------
subtotal is 40.0
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sThe passing test's output vanished; the failing test's output is attached to its report under Captured stdout call. With hundreds of tests, that is what you want — silence when all is well, evidence when it is not. To see everything as it happens, -s turns capturing off:
$ pytest -q -s --tb=no
subtotal is 45.0
.subtotal is 40.0
F
=========================== short test summary info ============================
FAILED test_print.py::test_loud - assert 40.0 == 45.0
1 failed, 1 passed in 0.01sExit codes
When pytest finishes, it hands the shell a number. CI systems decide "green" or "red" from that number alone:
| Code | Meaning | |---|---| | 0 | All collected tests passed | | 1 | Tests ran, and at least one failed | | 2 | The run was interrupted — an error during collection, or Ctrl+C | | 3 | An internal error inside pytest or a plugin | | 4 | A usage error — an unknown option, or a path or node ID that does not exist | | 5 | No tests were collected |
5 deserves attention. A -k that matches nothing, or a folder with no correctly named files, is not a pass — and CI treats it as a failure, which is exactly right:
$ pytest -q -k refund
6 deselected in 0.01s
$ echo $?
5The ModuleNotFoundError at the start of the chapter ended in Interrupted: 1 error during collection — exit code 2. A collection error is not a failing test; pytest never got as far as running tests.
A complete example
The plan is implemented and green. Now a slow test joins tests/test_cart.py, in a class of its own:
from shop.cart import Cart
class TestBigCart:
def test_many_items(self):
cart = Cart()
for _ in range(300_000):
cart.add("pen", 1.0)
assert cart.subtotal() == 300_000.0The file still holds TestCart above this class. First check what pytest sees, before running anything:
$ pytest --co -q
tests/test_cart.py::TestCart::test_empty_cart_subtotal
tests/test_cart.py::TestCart::test_subtotal_counts_quantity
tests/test_cart.py::TestCart::test_total_adds_tax
tests/test_cart.py::TestBigCart::test_many_items
tests/test_pricing.py::test_discount_ten_percent
tests/test_pricing.py::test_discount_zero
tests/test_pricing.py::test_add_tax_default_rate
7 tests collected in 0.01sSeven tests, each with an address. Run them all and ask for the slowest three:
$ pytest -q --durations=3
....... [100%]
============================= slowest 3 durations ==============================
0.04s call tests/test_cart.py::TestBigCart::test_many_items
(2 durations < 0.005s hidden. Use -vv to show these durations.)
7 passed in 0.12sOnly one test was slow enough to list. On a real project, --durations=10 is how you find the handful of tests that make the whole suite feel slow. While working on the cart, leave the slow one out:
$ pytest -q -k "cart and not many"
... [100%]
3 passed, 4 deselected in 0.01sThree things made this work. pythonpath = ["src"] lets from shop.cart import Cart succeed at all. TestBigCart is collected because its name starts with Test and it has no __init__. And -k "cart and not many" kept exactly the TestCart tests: cart matched everything in test_cart.py through the file name, and not many removed the slow one.
When it breaks
ModuleNotFoundError: No module named 'shop' during collection The code under test is not on sys.path. With a src/ layout, add pythonpath = ["src"] under [tool.pytest.ini_options], or install the package with pip install -e .. The run ends with Interrupted: 1 error during collection and exit code 2.
It works with python -m pytest but not with pytest python -m pytest also puts the current folder on sys.path; plain pytest does not. So a module in the project root is importable one way and not the other. Do not rely on the difference — set pythonpath, or install the package.
PytestCollectionWarning: cannot collect test class 'TestCart' because it has a __init__ constructor Test classes must not define __init__. Remove it and build what each test needs inside the test.
no tests ran, exit code 5 Nothing matched the discovery rules. Check the file name (test_*.py or *_test.py), the function names (test...), the class names (Test...). If you used -k, it may match nothing.
ERROR: not found: .../tests/test_cart.py::test_total_adds_tax The node ID is wrong. That test lives inside TestCart, so its address is tests/test_cart.py::TestCart::test_total_adds_tax. Copy node IDs from pytest --co -q rather than typing them. Exit code 4.
ERROR: file or directory not found: tests/test_carts.py A typo in the path. Exit code 4.
pytest: error: unrecognized arguments: --maxfial=2 A misspelt option; pytest --help lists the real ones. Exit code 4.
Step 4 of 6 — Predict
Check your understanding
This file is saved as test_rules.py. When you run pytest -q, how many passes does the last line report?
def test_found():
assert True
def check_total():
assert True
class TestGroup:
def test_method(self):
assert True
def helper(self):
assert True
class CartTests:
def test_ignored(self):
assert True- A3 passed
- B4 passed
- C2 passed
- D1 passed
The code lives in src/shop/pricing.py. pyproject.toml has pythonpath = ["shop"] under [tool.pytest.ini_options]. Yet pytest reports ModuleNotFoundError: No module named 'shop'. What is wrong?
# tests/test_pricing.py
from shop.pricing import add_tax
def test_add_tax_default_rate():
assert add_tax(100.0) == 115.0- AThe test file name must end in `_test.py`
- B`pythonpath` must point at the folder that *contains* the `shop` package: `["src"]`
- CThe function name must start with `Test`
- D`tests/` needs an `__init__.py` before anything can be imported
Two tests were failing and you have fixed the bug. Which command reruns only those two tests?
- Apytest --ff
- Bpytest -x
- Cpytest --co
- Dpytest --lf
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
Your turn
A library charges late fees. Build this project:
library/
├── src/
│ └── library/
│ ├── __init__.py
│ └── fees.py
└── tests/
└── test_fees.pysrc/library/fees.py:
def late_fee(days_late, daily_rate=5):
if days_late < 0:
raise ValueError(f"days_late cannot be negative: {days_late}")
return min(days_late * daily_rate, 100)- Write a test plan first: on time, a few days late, exactly at the cap, past the cap, a custom daily rate.
- Write
tests/test_fees.pywith those cases as methods of one class,TestLateFee. Runpytestwithout apyproject.toml, read the error, and checkecho $?. - Add
pyproject.tomlso the tests can importlibrary. Get to green. - List the node IDs with
--co -q. Run only the "past the cap" test by its node ID. - Use
-kto run only the tests about the cap, then everything except them. - Change the cap in
fees.pyfrom100to90. Run the suite, then--lf. Put it back and run--lf, then everything.
Solution
The plan:
| Case | Input | Expected | |---|---|---| | On time | late_fee(0) | 0 | | A few days late | late_fee(3) | 15 | | Boundary: exactly at the cap | late_fee(20) | 100 | | Past the cap | late_fee(30) | 100 | | Custom rate | late_fee(3, daily_rate=2) | 6 | | Negative days | late_fee(-1) | ValueError — later, with pytest.raises |
tests/test_fees.py:
from library.fees import late_fee
class TestLateFee:
def test_on_time_costs_nothing(self):
assert late_fee(0) == 0
def test_three_days_late(self):
assert late_fee(3) == 15
def test_fee_reaches_cap_exactly(self):
assert late_fee(20) == 100
def test_fee_never_exceeds_cap(self):
assert late_fee(30) == 100
def test_custom_daily_rate(self):
assert late_fee(3, daily_rate=2) == 6Without configuration, the run stops at collection with ModuleNotFoundError: No module named 'library', Interrupted: 1 error during collection, and echo $? prints 2. Then pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]$ pytest -q
..... [100%]
5 passed in 0.01s
$ pytest --co -q
tests/test_fees.py::TestLateFee::test_on_time_costs_nothing
tests/test_fees.py::TestLateFee::test_three_days_late
tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly
tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
tests/test_fees.py::TestLateFee::test_custom_daily_rate
5 tests collected in 0.01s
$ pytest -q tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap
. [100%]
1 passed in 0.01s
$ pytest -q -k cap
.. [100%]
2 passed, 3 deselected in 0.01s
$ pytest -q -k "not cap"
... [100%]
3 passed, 2 deselected in 0.01sWith the cap changed to 90:
$ pytest -q --tb=no
..FF. [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 passed in 0.01s
$ pytest -q --lf --tb=no
FF [100%]
=========================== short test summary info ============================
FAILED tests/test_fees.py::TestLateFee::test_fee_reaches_cap_exactly - assert...
FAILED tests/test_fees.py::TestLateFee::test_fee_never_exceeds_cap - assert 9...
2 failed, 3 deselected in 0.01sWith the cap back at 100, pytest -q --lf gives 2 passed, 3 deselected, and a full pytest -q gives 5 passed.
Why these decisions:
- Two cap tests, not one.
late_fee(20)sits exactly on the boundary, the first day the fee reaches the cap;late_fee(30)is well past it. Off-by-one mistakes live at boundaries, so the boundary gets its own test — and the second one proves the cap still holds beyond it. - The word
capis in both cap test names, and only there. That is what made-k capand-k "not cap"select cleanly — names were chosen with selection in mind. - One class, no
__init__, no shared state. Each method callslate_feeitself; the class only gives the group one address,tests/test_fees.py::TestLateFee. pythonpathinstead of asys.pathline in the test file, so the tests pass from any folder you run them in.- The full run after
--lf.--lfproves the two red tests are green again; only the full run proves the other three are still green.
Step 6 of 6
Stretch — the chapter quiz
Ten questions from easy to hard. The last ones are difficult on purpose.
Sign in to take the quiz