Skip to content

Commit eb8e47b

Browse files
authored
Merge pull request #48 from michalmaj/test/add-coverage
test: add local coverage reporting
2 parents d8937f4 + d1853e5 commit eb8e47b

12 files changed

Lines changed: 314 additions & 16 deletions

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ By following the guide, you will learn how to:
2929
- configure the project in `pyproject.toml`,
3030
- install a package in editable mode and build distributions,
3131
- test code with `pytest`,
32+
- inspect statement and branch coverage with pytest-cov,
3233
- lint and format code with Ruff,
3334
- check type annotations with mypy,
3435
- run automated quality checks in GitHub Actions,
@@ -53,7 +54,8 @@ understand why each tool and file is introduced.
5354
9. [Common beginner mistakes](docs/08_common_mistakes.md)
5455
10. [Building distributions](docs/10_building_distributions.md)
5556
11. [Static type checking with mypy](docs/11_type_checking_with_mypy.md)
56-
12. [Project checklist](docs/09_checklist.md)
57+
12. [Test coverage with pytest-cov](docs/12_test_coverage.md)
58+
13. [Project checklist](docs/09_checklist.md)
5759

5860
## Reference material
5961

‎docs/01_project_structure.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ In this guide, it contains:
116116
- dependency configuration,
117117
- pytest configuration,
118118
- mypy configuration,
119+
- coverage configuration,
119120
- Ruff configuration.
120121

121122
Keeping these settings together makes the project easier to inspect and maintain.

‎docs/02_uv.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ uv sync
105105
uv add --group dev pytest
106106
uv add --group dev ruff
107107
uv add --group dev mypy
108+
uv add --group dev pytest-cov
108109
uv run pytest
110+
uv run pytest --cov=text_toolkit --cov-report=term-missing
109111
uv run ruff check .
110112
uv run ruff format .
111113
uv run mypy

‎docs/03_pyproject_toml.md‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ build-backend = "uv_build"
4141
dev = [
4242
"mypy>=2.3.1",
4343
"pytest>=9.0.3",
44+
"pytest-cov>=7.1.0",
4445
"ruff>=0.15.12",
4546
]
4647

@@ -52,6 +53,13 @@ python_version = "3.12"
5253
files = ["src", "tests"]
5354
strict = true
5455

56+
[tool.coverage.run]
57+
branch = true
58+
source = ["text_toolkit"]
59+
60+
[tool.coverage.report]
61+
show_missing = true
62+
5563
[tool.ruff]
5664
line-length = 88
5765
target-version = "py312"
@@ -68,7 +76,7 @@ select = [
6876
```
6977

7078
This describes the project, its build system, its development dependencies, and
71-
the settings used by pytest, mypy, and Ruff.
79+
the settings used by pytest, mypy, Coverage.py, and Ruff.
7280

7381
## The `[project]` section
7482

@@ -136,8 +144,8 @@ This is intentional.
136144

137145
The example package starts with standard library code only.
138146

139-
Development tools such as `pytest`, mypy, and Ruff are stored separately in the
140-
`dev` dependency group.
147+
Development tools such as `pytest`, pytest-cov, mypy, and Ruff are stored
148+
separately in the `dev` dependency group.
141149

142150
## The `[build-system]` section
143151

@@ -182,6 +190,7 @@ Development dependencies are needed only while developing the project.
182190
Examples of development dependencies:
183191

184192
- `pytest`,
193+
- `pytest-cov`,
185194
- `mypy`,
186195
- `ruff`,
187196
- test coverage tools,
@@ -209,6 +218,12 @@ and:
209218

210219
and:
211220

221+
```toml
222+
[tool.coverage.run]
223+
```
224+
225+
and:
226+
212227
```toml
213228
[tool.ruff]
214229
```
@@ -231,7 +246,8 @@ This guide was built using a slower approach:
231246
6. configure linting and formatting,
232247
7. add continuous integration,
233248
8. add a build system and install the project,
234-
9. add static type checking.
249+
9. add static type checking,
250+
10. add test coverage reporting.
235251

236252
Each step should explain one idea clearly.
237253

‎docs/09_checklist.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,5 +274,5 @@ Let the project grow gradually.
274274

275275
## Continue
276276

277-
- [Previous: Static Type Checking with mypy](11_type_checking_with_mypy.md)
277+
- [Previous: Test Coverage with pytest-cov](12_test_coverage.md)
278278
- [Back to README](../README.md)

‎docs/11_type_checking_with_mypy.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,5 @@ Use pytest to check what the code actually does when it runs.
120120
## Continue
121121

122122
- [Previous: Building Distributions](10_building_distributions.md)
123-
- [Next: Project Checklist](09_checklist.md)
123+
- [Next: Test Coverage with pytest-cov](12_test_coverage.md)
124124
- [Back to README](../README.md)

‎docs/12_test_coverage.md‎

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Test Coverage with pytest-cov
2+
3+
Test coverage measures which parts of the source code run while the test suite
4+
is executing.
5+
6+
It can reveal code that no test reaches, but it cannot prove that the tests make
7+
the right assertions.
8+
9+
## Coverage is a diagnostic tool
10+
11+
A coverage report helps answer questions such as:
12+
13+
- Which source lines ran during the tests?
14+
- Which branches of a condition were exercised?
15+
- Is there production code that the test suite never reaches?
16+
17+
It does not answer:
18+
19+
- Are the assertions correct?
20+
- Are important requirements missing?
21+
- Are the tests readable and maintainable?
22+
- Is the program design good?
23+
24+
Coverage provides evidence about test execution, not a complete quality score.
25+
26+
## Add pytest-cov
27+
28+
This project uses pytest-cov to collect coverage while pytest runs. It is a
29+
development dependency:
30+
31+
```bash
32+
uv add --group dev pytest-cov
33+
```
34+
35+
The package is not needed by users of `text-toolkit`.
36+
37+
## Configure coverage
38+
39+
Coverage.py reads its settings from `pyproject.toml`:
40+
41+
```toml
42+
[tool.coverage.run]
43+
branch = true
44+
source = ["text_toolkit"]
45+
46+
[tool.coverage.report]
47+
show_missing = true
48+
```
49+
50+
The settings mean:
51+
52+
- `source` limits measurement to the example package,
53+
- `branch` records possible paths through conditions,
54+
- `show_missing` lists source lines that did not run.
55+
56+
Tests themselves are not included in the percentage. The report measures the
57+
package behavior that the tests exercise.
58+
59+
## Statement and branch coverage
60+
61+
Statement coverage checks whether executable statements ran.
62+
63+
Branch coverage also checks different paths through decisions. For example, an
64+
`if` statement may execute both its true and false branches. Running the line
65+
with the `if` is not enough to prove that both paths were exercised.
66+
67+
The example package has a condition in `count_words`. Tests cover both an empty
68+
input and text containing words, so both outcomes are represented.
69+
70+
## Run the coverage report
71+
72+
Run:
73+
74+
```bash
75+
uv run pytest --cov=text_toolkit --cov-report=term-missing
76+
```
77+
78+
pytest still runs the same tests. pytest-cov additionally records execution and
79+
prints a table similar to:
80+
81+
```text
82+
Name Stmts Miss Branch BrPart Cover Missing
83+
----------------------------------------------------------------------------
84+
src/text_toolkit/__init__.py 2 0 0 0 100%
85+
src/text_toolkit/text_stats.py 11 0 4 0 100%
86+
----------------------------------------------------------------------------
87+
TOTAL 13 0 4 0 100%
88+
```
89+
90+
The important columns are:
91+
92+
- `Stmts`: executable statements found,
93+
- `Miss`: statements that did not run,
94+
- `Branch`: branch destinations found,
95+
- `BrPart`: partially covered branches,
96+
- `Missing`: source line numbers that did not run.
97+
98+
The exact counts will change when the package changes.
99+
100+
## Why there is no percentage gate
101+
102+
This project intentionally does not configure `fail_under`.
103+
104+
A fixed threshold can encourage tests written only to increase a number. A
105+
suite can reach 100% coverage while using weak assertions or missing important
106+
requirements.
107+
108+
The current report happens to show 100%, but that is a property of a very small
109+
example package. It is not a promise that every future change must preserve a
110+
perfect score.
111+
112+
When coverage decreases, inspect the missing lines and decide whether they
113+
represent meaningful untested behavior. Do not add a test merely to satisfy a
114+
percentage.
115+
116+
## Generated data
117+
118+
Coverage.py stores local measurement data in:
119+
120+
```text
121+
.coverage
122+
```
123+
124+
This file is generated and listed in `.gitignore`. Do not commit it.
125+
126+
## Rule of thumb
127+
128+
Use coverage to find questions worth asking about the test suite.
129+
130+
Do not use the percentage as a substitute for those questions.
131+
132+
## Continue
133+
134+
- [Previous: Static Type Checking with mypy](11_type_checking_with_mypy.md)
135+
- [Next: Project Checklist](09_checklist.md)
136+
- [Back to README](../README.md)

‎docs/ROADMAP.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ These parts are already included in the repository.
8888
- [x] Add mypy as a development dependency
8989
- [x] Configure strict local type checking
9090
- [x] Run type checks against source code and tests
91+
- [x] Add pytest-cov and local branch coverage reporting
9192

9293
### Continuous integration
9394

@@ -130,11 +131,17 @@ These parts are already included in the repository.
130131
- [x] Review commit history and self-review coverage in the existing guides
131132
- [x] Standardize terminology and dependency examples across the documentation
132133
- [x] Explain type hints and static type checking with mypy
134+
- [x] Explain what coverage does and does not measure
135+
- [x] Avoid treating coverage percentage as a quality guarantee
133136

134137
## Next
135138

136139
These are good candidates for upcoming small pull requests.
137140

141+
### Test coverage
142+
143+
- [ ] Run coverage reporting in CI and align workflow documentation
144+
138145
### Repository polish
139146

140147
- [ ] Add issue templates only if the repository starts receiving external feedback
@@ -155,12 +162,6 @@ These topics are intentionally not part of the first foundation.
155162

156163
They may be added later as separate chapters.
157164

158-
### Test coverage
159-
160-
- [ ] Add coverage tooling
161-
- [ ] Explain what coverage does and does not mean
162-
- [ ] Avoid treating coverage percentage as a quality guarantee
163-
164165
### Pre-commit hooks
165166

166167
- [ ] Add pre-commit hooks

‎docs/common_questions.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,17 +253,20 @@ Readers can now learn those foundations first and then continue to the
253253
[type-checking chapter](11_type_checking_with_mypy.md). This order keeps type
254254
hints and the tool that checks them from becoming another unexplained layer.
255255

256-
## Why not add coverage yet?
256+
## Why was coverage added after the testing basics?
257257

258258
Coverage tools can show which lines are executed by tests.
259259

260260
However, coverage can be misleading if introduced too early.
261261

262262
High coverage does not automatically mean good tests.
263263

264-
This guide first focuses on writing clear tests that check behavior.
264+
This guide first focuses on writing clear tests that check behavior. It then
265+
introduces coverage as a diagnostic tool in the
266+
[coverage chapter](12_test_coverage.md).
265267

266-
Coverage can be added later after the testing basics are clear.
268+
The project deliberately has no percentage gate. Readers should inspect missing
269+
behavior instead of treating a high number as proof of test quality.
267270

268271
## Why does CI use uv sync --locked --group dev?
269272

‎docs/glossary.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ uv build --no-sources
4747

4848
CI helps protect the `main` branch from broken changes.
4949

50+
## Coverage
51+
52+
Coverage measures which source statements and branches run while tests execute.
53+
54+
This project generates a terminal report with:
55+
56+
```bash
57+
uv run pytest --cov=text_toolkit --cov-report=term-missing
58+
```
59+
60+
A high percentage does not prove that tests use meaningful assertions or cover
61+
every requirement. Coverage is evidence about execution, not a complete quality
62+
score.
63+
5064
## Commit
5165

5266
A commit is a saved change in Git history.

0 commit comments

Comments
 (0)