2025, Sep 23 13:00

Fixing missing IntegrityError in Django tests: constraints ignored on unmanaged models (managed = False)

Learn why Django Meta constraints don’t trigger IntegrityError on unmanaged models (managed = False) and how to enforce Check/Unique constraints in the database

When database constraints in a Django model refuse to raise an IntegrityError during tests, the instinct is to blame the Q expressions or the test setup. But if the model is declared with managed = False, Django won’t touch the table schema at all. In that case, constraints defined on the model simply never make it to the database, and there’s nothing for the database to enforce. The result: no exception on save(), no matter how invalid the data is.

Problem setup

Consider a model that declares several constraints, including a CheckConstraint to restrict status and a rule preventing date_published from being set when the product is in Draft or Discontinued. The model also includes a set of UniqueConstraint declarations. Tests attempt to trigger an IntegrityError by violating the constraints.

from django.db import models
from django.db.models import CheckConstraint, Q, UniqueConstraint
class CatalogItem(models.Model):
    code = models.CharField(primary_key=True, max_length=8)
    ean = models.CharField(unique=True, max_length=14, blank=True, null=True)
    title = models.TextField(blank=True, null=True)
    msrp = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
    state = models.TextField()
    maker = models.TextField(blank=True, null=True)
    label = models.TextField(blank=True, null=True)
    origin = models.TextField(blank=True, null=True)
    updated_at = models.DateField(blank=True, null=True)
    published_at = models.DateField(blank=True, null=True)
    is_exclusive = models.BooleanField(blank=True, null=True)
    class Meta:
        managed = False
        db_table = 'product'
        constraints = [
            CheckConstraint(
                condition=Q(state__in=['Draft', 'Live', 'Discontinued']),
                name='ck_state_allowed',
                violation_error_message="status field must be on of the following: 'Draft', 'Live', 'Discontinued'",
            ),
            CheckConstraint(
                condition=~(Q(published_at__isnull=False) & Q(state__in=['Draft', 'Discontinued'])),
                name='ck_published_vs_state',
                violation_error_message="Product with status 'Draft' or 'Discontinued' cannot have a date_published value",
            ),
            UniqueConstraint(fields=['ean'], name='uq_ean'),
            UniqueConstraint(fields=['code', 'is_exclusive'], name='uq_code_exclusive'),
            UniqueConstraint(fields=['code', 'state'], name='uq_code_state'),
        ]

A test tries to save an invalid state value and expects an IntegrityError:

from django.db.utils import IntegrityError
from django.test import TestCase
from .models import CatalogItem
class CatalogItemTests(TestCase):
    def setUp(self):
        self.item = CatalogItem.objects.create(
            code='12345678',
            ean='5666777888999',
            title='Test Product 1',
            msrp=999,
            state='Live',
            maker='',
            label='Test Brand',
            origin='China',
            updated_at='2025-01-01',
            published_at='2025-01-01',
            is_exclusive=False,
        )
    def test_invalid_state_rejected(self):
        self.item.state = 'WrongStatus'
        with self.assertRaises(IntegrityError):
            self.item.save()

What’s actually happening

In Django, constraints declared in Meta.constraints are enforced by the database. Django translates a CheckConstraint into a database-level CHECK, for example CHECK status IN ('Draft', 'Live', 'Discontinued'). The database decides whether the row can be inserted or updated, and if not, it raises an integrity error that Django surfaces as django.db.utils.IntegrityError.

But managed = False changes the picture completely. With managed disabled, Django does not create, alter, or drop the underlying table or any of its constraints. It will not add fields, will not add CHECKs, and will not add UNIQUEs. As a result, the constraints listed in the model are not present in the database and therefore cannot trigger.

Solution

If the model is unmanaged, those Django-declared constraints won’t be applied to the database schema. To make them effective, add the equivalent constraints directly to the database. For example, the status restriction becomes a native CHECK constraint at the database level, which in SQL would look like:

CHECK (status IN ('Draft', 'Live', 'Discontinued'))

The same applies to the other constraints: enforce them directly in the database so that PostgreSQL 14.18 can reject invalid rows and raise an integrity error.

If the database owns the contract, save() will propagate violations as IntegrityError, and tests that expect those violations can rely on the database to enforce them.

Why this matters

Constraints are only as strong as their enforcement point. In Django, Meta.constraints codifies the intent and, when managed tables are in play, drives migrations that install those rules in the database. When managed is off, that link is cut. The code still advertises the constraints, but the database knows nothing about them, so nothing fails at write time. Understanding that boundary prevents false assumptions in testing and production.

Takeaways

Database constraints declared in Django are enforced by the database, not by Python code in save(). With managed = False, Django will not synchronize those constraints to the table, so they won’t trigger in tests or production. To rely on IntegrityError from these rules, add the corresponding constraints directly to the database so they exist where enforcement actually happens.

The article is based on a question from StackOverflow by dabo_tusev and an answer by willeM_ Van Onsem.