Verdict
Yes
Test the rules you write into a model, such as validations, calculated values and query scopes, by saving and reading records in a test database; do not test the field declarations that the ORM maps for you.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Test the rules you put into a model, and leave the field mapping to the ORM. The typical case is a Django, Rails or Laravel model with a few fields, two or three validations and a method, such as a booking that counts its nights. Blast radius is users and Change frequency is regularly, because a wrong rule shows customers wrong data and models gain rules with most features. Detectability is eventually, because a broken validation saves invalid records without an error. Reversibility is with-effort, because bad records stay until a repair script fixes them, and Test cost is moderate, because a model test needs a test database, and rule R11 gives Test.
| When | Decision | Why |
|---|---|---|
| The model only declares fields that map to table columns, with no validations or methods | Do not test the model; test the code that reads and writes its fields | Detectability moves to immediately, Reversibility to trivial and Test cost to trivial, because the ORM or the migration check rejects a field that does not match the table |
| A model method calculates an amount that customers pay, such as an invoice total | Test mandatory: the main path, every known failure and the boundary amounts | Blast radius rises to money and Reversibility to costly |
| A default scope limits every query to the records of the current tenant | Test mandatory, including a query that must not return the records of another tenant | Blast radius rises to safety-or-legal and Detectability to never, because a broken scope returns other customers' data without an error |
| A NOT NULL, CHECK or unique constraint in the database also enforces the model rule | Test minimally: one test that the database rejects an invalid record | Detectability moves to same-day and Reversibility to trivial, because the database refuses the bad row and stores nothing |
| Saving one valid record in a test needs a dozen related records and seeded reference data | Test it differently: a nightly query counts records that break the model rule and alerts above zero | Test cost rises to heavy while Detectability stays eventually and Reversibility with-effort |
What breaks if you don't test
A validation deleted during a refactor, or skipped by one save path, lets in records the rest of the code assumes cannot exist: a booking that ends before it starts, an order without a customer. Nothing crashes on save. The failure shows days later in a report or a calendar, and every record saved since the change needs a repair.
What you lose if you over-test
The common over-test asserts what the ORM guarantees: that name is a CharField with max_length=100, or that user.orders returns orders. These tests repeat the model definition, so every new field costs two edits. A mocked ORM wastes time too: the test checks your mock, and a scope with a wrong filter passes it.
How to test
Write integration tests against the database engine that production runs, inside a transaction the test framework rolls back, as Django's TestCase and Rails model tests do. For each validation, save one valid and one invalid record through the call that production code uses. For each calculated value, test the main input and the boundaries; for each query scope, create one record that matches and one that must not. See the Django testing overview and the Rails guide on testing models.
When the answer changes
- A model method calculates money, or a scope decides whose records a user may see.
- The model holds no rules, which makes it a data transfer object with a table behind it.
- Building one valid record in a test takes more setup than the rule you check.
Real incident + Code example
The bookings that skipped validation
On a Django booking service I worked on, the Booking model checked in clean() that the end date came after the start date, and its test called full_clean() and passed. The partner import job used Booking.objects.create(), and Django does not call full_clean() from save(), as its validation documentation states. For three weeks the job saved 37 bookings with reversed dates, the calendar showed those rooms as free, and we found out when two guests arrived for the same room. We moved the rule into a CheckConstraint, repaired the records by hand, and made the test save through the call the import job makes:
from datetime import date
from django.db import IntegrityError, transaction
from django.test import TestCase
from bookings.models import Booking, Room
class BookingModelTest(TestCase):
def test_create_rejects_end_before_start(self):
room = Room.objects.create(number="101")
with self.assertRaises(IntegrityError), transaction.atomic():
Booking.objects.create(
room=room, start=date(2026, 9, 20), end=date(2026, 9, 18)
)
def test_nights_counts_each_night_once(self):
booking = Booking(start=date(2026, 9, 20), end=date(2026, 9, 23))
self.assertEqual(booking.nights(), 3)
The constraint holds for every path that writes a booking, including bulk_create().
Related questions
FAQ
- Should model classes be tested?
Yes, test a model class when it holds rules such as validations, calculated values or query scopes. A model class that only declares fields needs no test of its own, because tests of the code that reads the fields fail when a field is wrong.
- Should Django models be tested?
Yes, test the validations, methods and custom managers you add to a Django model, using
django.test.TestCase. Do not test field options such asmax_length, because Django's own suite covers them andmakemigrations --checkin CI catches a model that no longer matches its migrations.- Should model tests use a real database?
Yes, model tests should use a real database of the engine production runs. A mocked ORM cannot check a query filter or a unique constraint, and SQLite ignores
VARCHARlengths, so a string that PostgreSQL rejects passes on SQLite.- Should I test model validations?
Yes, test each validation with one valid and one invalid record, saved through the call production code uses. When the rule fits a database constraint, add the constraint too, because a model validation runs only on the save paths that call it.