Skip to content

Commit 71df779

Browse files
Nickatakclaude
andcommitted
docs: Document test shape convention
Adds a `Test shape` section to `docs/developer/backend.md` between `Serializer shape` and `Auth`, capturing the per-resource file layout, the factory-helpers pattern in `common.py`, and the test_<subject>_<action>_<expectation> method-name form. Updates the test section in `backend-docstring-style.md` to point at the new layout and show the factory-helpers + setUp shape; the behavioral-docstring guidance is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 070ed90 commit 71df779

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

docs/developer/backend-docstring-style.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,25 @@ See [`backend/ctj_api/urls.py`](https://github.com/hackforla/CivicTechJobs/blob/
178178

179179
### Tests — light template, behavioral test names
180180

181-
Tests get a module docstring and a class docstring (one-liner each). Each test method's docstring is a behavioral statement of what the test verifies, not a label for the method.
181+
Tests live one file per resource under [`backend/ctj_api/tests/`](https://github.com/hackforla/CivicTechJobs/tree/main/backend/ctj_api/tests) (see [`backend.md`](backend.md)'s `Test shape` section for the full layout rule). Each file holds one `<Resource>Tests` class extending `APITestCase`; shared fixture-construction lives in `common.py` as factory helpers.
182+
183+
Test method names follow `test_<subject>_<action>_<expectation>`. Each method's docstring is a behavioral statement of what the test verifies, not a label for the method.
182184

183185
```python
184186
"""<one-liner: what this test module covers>."""
185187

186188

187189
class FooTests(APITestCase):
188-
"""<one-liner: what this test class scopes to>."""
190+
"""<one-liner: what this test class scopes to>.
191+
192+
[Optional: prose paragraph for the resource's policy
193+
surface, e.g. "Reads are public; mutations gated by ...".]
194+
"""
195+
196+
def setUp(self):
197+
...
189198

190-
def test_some_behavior(self):
199+
def test_<subject>_<action>_<expectation>(self):
191200
"""<Subject> <verb phrase asserting expected outcome>."""
192201
```
193202

docs/developer/backend.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,36 @@ def get_serializer_class(self):
115115

116116
FBVs reference the right serializer directly — they only do one thing, so they only ever need one class.
117117

118+
## Test shape
119+
120+
Tests live in [backend/ctj_api/tests/](https://github.com/hackforla/CivicTechJobs/tree/main/backend/ctj_api/tests), one file per resource:
121+
122+
```
123+
ctj_api/tests/
124+
├── common.py — factory helpers, no test classes
125+
├── test_healthcheck.py
126+
├── test_users.py
127+
├── test_opportunities.py
128+
├── test_community_of_practice.py
129+
├── test_roles.py
130+
├── test_skills.py
131+
└── test_projects.py
132+
```
133+
134+
Each file holds one `<Resource>Tests` class extending `APITestCase`. Each class has its own `setUp` that constructs only the rows its tests need — there is no shared `APITestBase` because there is no meaningful universal setup (healthcheck needs nothing; user-detail needs users; opportunity tests need users + reference rows).
135+
136+
Shared fixture-construction lives in `common.py` as factory functions (`make_pm_user`, `make_regular_user`, `make_cop`, `make_role`, `make_skill`, `make_project`, `make_opportunity`). Each call returns a saved instance; defaults are sensible and overridable via keyword arguments. The factories don't share state — each call creates a new row.
137+
138+
Test method names follow `test_<subject>_<action>_<expectation>`:
139+
140+
- subject — who's making the request (`anonymous`, `regular_user`, `pm_user`, `authenticated_user`)
141+
- action — what's being attempted (`list`, `create`, `view_own_record`)
142+
- expectation — the result (`returns_200`, `cannot_create`, `gets_403`)
143+
144+
Examples: `test_anonymous_can_list_opportunities`, `test_regular_user_cannot_create_opportunity`, `test_authenticated_user_cannot_view_others_record`. Each test method also has a one-liner docstring describing the assertion in plain English.
145+
146+
When a resource grows write tests alongside read tests (and the file gets long), split into multiple classes within the same file: `<Resource>ReadTests`, `<Resource>WriteTests`. Don't split into separate files unless the test count justifies it.
147+
118148
## Auth
119149

120150
**Stage 1** - Django's default session authentication. The DRF API uses `SessionAuthentication`; the app frontend signs in through a Django-issued session cookie. `createsuperuser` is the bootstrap path for admin / PM accounts. Sessions over token auth here because Django admin already uses sessions, so reusing them keeps Stage 1 free of extra auth infrastructure.

0 commit comments

Comments
 (0)