Improvement: model unit tests, from 3 of 17 models covered to all of them¶
Summary: Adds six
test_models_*.pymodules mirroring themodels/package, shared model factories inconftest.pybuilt from the realtests/data/exports, and fixes theNoReverseMatchinPost.get_absolute_url()that the new tests exposed.
π Before¶
src/parodynews/models/ holds 17 model classes across six modules. Only three β Thread, Message, AppConfig β were referenced by any model-level test, all of them incidentally, inside test_thread_message_delete.py. test_model_table.py's 15 tests exercise the model-table view, which is a different guarantee, and test_templates.py's 38 are template tests.
Statement coverage of src/parodynews/models/ was 85%, and base.py was at 0%.
β Change¶
Six test modules, mirroring the package¶
| module | covers |
|---|---|
test_models_base.py |
TimestampedModel, DisplayFieldsMixin |
test_models_config.py |
PoweredBy, AppConfig, FieldDefaults |
test_models_ai.py |
JSONSchema, OpenAIModel, Assistant, AssistantGroup, AssistantGroupMembership |
test_models_content.py |
ContentDetail, ContentItem |
test_models_conversation.py |
Thread, Message |
test_models_publishing.py |
PostPageConfigModel, Post, PostFrontMatter, PostVersion |
Each asserts field definitions and defaults, __str__, get_display_fields(), Meta.ordering, every ForeignKey/ManyToMany/OneToOne, and the on_delete behaviour of each relation β CASCADE and SET_NULL are asserted separately, because which one a relation uses is the difference between losing a published post and keeping it.
The two custom save() methods get behavioural tests rather than does-not-raise tests:
ContentItem.save()numbers items per parent detail. An implementation based on a globalcount()would pass a single-parent test; the addedtest_save_numbers_each_detail_independentlyfails it.FieldDefaults.save()exists only tocache.delete("field_defaults"). It is asserted on insert, on update, and β separately β that it does not evict other keys, which is what acache.clear()regression would do.
Shared factories, built from the real exports¶
conftest.py gains *_export fixtures that read tests/data/*.json as-is and a chain of model factories (user β openai_model β assistant β content_detail β content_item β thread β message β post). No parallel fixture mechanism and no new dependency β factory_boy was deliberately not added.
TimestampedModel is the one class in the package that is not instantiated: it is abstract = True and has no concrete subclass in the tree (OpenAIModel and Post declare their own timestamp columns), so it is asserted through its field definitions and its abstractness.
A completeness guard¶
test_every_exported_model_has_a_test_module asserts that every name in parodynews.models.__all__ is referenced by one of the six modules, so a model added later without tests fails the suite instead of passing quietly.
π Defect found and fixed¶
Post.get_absolute_url() raised on every call:
# Before
return reverse("post_detail", kwargs={"pk": self.pk})
# django.urls.exceptions.NoReverseMatch: Reverse for 'post_detail' with keyword
# arguments '{'pk': 5}' not found. 1 pattern(s) tried:
# ['posts/(?P<post_id>[0-9]+)/\\Z']
# After
return reverse("post_detail", kwargs={"post_id": self.pk})
parodynews/urls.py:161 declares the route as posts/<int:post_id>/. The method was never called by a test, and this is precisely the class of defect issue #51 was filed to surface. It is a one-word fix in models/publishing.py; no migration, no signature change.
π§ͺ Testing and Validation¶
Run from src/ (config: src/pytest.ini, e2e excluded by default) against the project's PostgreSQL test database β base.py rejects SQLite outright:
$ python -m pytest parodynews/tests -q
56 passed, 9 deselected # before
173 passed, 9 deselected # after
Statement coverage of src/parodynews/models/:
| module | before | after |
|---|---|---|
__init__.py |
100% | 100% |
ai.py |
89% | 100% |
base.py |
0% | 100% |
config.py |
87% | 100% |
content.py |
79% | 100% |
conversation.py |
95% | 100% |
publishing.py |
87% | 100% |
| TOTAL | 85% | 100% |
ruff check and ruff format --check pass on every added and changed file.
β οΈ Breaking Changes and Migration¶
None. No model field changed, no migration is required, and no dependency was added to pyproject.toml.
π Related Resources¶
- Issue: #51 (parent: #50)
- Package:
src/parodynews/models/README.md - Test conventions:
src/parodynews/tests/README.md - Fixture exports:
src/parodynews/tests/data/README.md - Route:
src/parodynews/urls.pyβpost_detail - Django testing topics