Fix Django Tests That Pass Locally but Fail in CI
Fix Django test suites that pass on your machine but fail in GitHub Actions or GitLab CI: PostgreSQL service containers and health checks, missing settings and secrets, static files manifest errors, time zones, and test ordering.
Problem statement
python manage.py test (or pytest) is green on your laptop and red in CI. The failures vary: could not connect to server, permission denied to create database, ValueError: Missing staticfiles manifest entry, tests that depend on dates, or a test that only fails when run with the whole suite.
CI runs your code in a clean environment: a fresh database service that may not be ready yet, no .env file, a different time zone, no collected static files, and often a different test order or parallelism. Each difference exposes an assumption your local setup was quietly satisfying.
- Run PostgreSQL as a service with a health check, and set
DATABASE_URLto it. - Give the test role permission to create databases (
CREATEDB), since Django createstest_<name>. - Provide required settings as CI variables or a test settings module; never depend on a local
.env. - Use
StaticFilesStorage(not the manifest storage) in test settings, or runcollectstaticfirst. - Pin
TIME_ZONEand usefreezegunortime_machinefor date-dependent tests. - Reproduce order bugs locally with
--shuffleand--parallel.
Step-by-step solution
1. Run PostgreSQL as a healthy service
GitHub Actions:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s --health-timeout 5s --health-retries 10
env:
DATABASE_URL: postgres://app:app@localhost:5432/app
DJANGO_SETTINGS_MODULE: config.settings.test
SECRET_KEY: test-only-not-secret
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- run: python manage.py test --parallel
Without the health options, tests can start before PostgreSQL accepts connections. In GitLab CI, use services: [postgres:16] and connect to host postgres, not localhost.
2. Allow creating the test database
Django connects to the configured database and creates test_app. The official image's POSTGRES_USER is a superuser, so this works there. On a managed or shared CI database, grant it:
ALTER ROLE app CREATEDB;
permission denied to create database is the symptom otherwise.
3. Make settings explicit
A config/settings/test.py that imports base settings and overrides what tests need removes dependence on local files:
from .base import * # noqa
DEBUG = False
SECRET_KEY = "test-only-not-secret"
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] # fast tests
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
CELERY_TASK_ALWAYS_EAGER = True
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.InMemoryStorage"},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}
ManifestStaticFilesStorage (or WhiteNoise's compressed manifest storage) requires collectstatic to have run; template rendering in tests then fails with a manifest error. Plain StaticFilesStorage avoids that. Using a fast password hasher can halve suite time.
4. Remove time and locale assumptions
CI runners usually run in UTC. Tests that compute "today" or compare formatted dates can fail around midnight or in a different locale. Freeze time:
import time_machine
@time_machine.travel("2026-10-02 12:00:00+00:00", tick=False)
def test_invoice_due_date(self):
...
5. Find order-dependent tests
python manage.py test --shuffle --parallel 4
python manage.py test --shuffle 12345 --verbosity 2 # reproduce a specific order
Tests that pass alone but fail together usually share state: a module-level cache, a mocked object never restored, data created in setUpClass and mutated, or a test using TransactionTestCase that leaves rows behind.
Verification check: run the CI job twice in a row and with --shuffle; it should pass both times. Locally, run the suite with the CI environment variables (env -i plus the same exports) to confirm nothing depends on your shell.
Explanation
A test suite is only as reproducible as its environment. Local development accumulates state: a long-running database with the right roles, environment variables in a shell profile or .env, collected static files, and a familiar time zone. CI starts from nothing on every run, which is exactly its value: it reveals the implicit inputs that would also be missing on a new developer's machine or a fresh production server.
Edge cases / notes
- PostGIS or extensions. Use an image with the extension (
postgis/postgis:16-3.4) and make sure migrations create it. - Parallel tests create one database per process (
test_app_1,test_app_2); the role needsCREATEDBfor all of them. - Caching dependencies with
actions/setup-python'scache: pipspeeds up runs significantly. - Docker-based CI (running tests inside your production image) catches missing system libraries early.
Next steps
- Add deployment to the same workflow with Deploy Django With GitHub Actions CI/CD.
- Static manifest errors in production are covered in Fix Missing staticfiles Manifest Entry.
FAQ
Why do my tests fail with could not connect to server in CI?
The database service is not ready yet or the host is wrong. Add a health check and use the right hostname.
Should CI use SQLite to be faster?
It is faster but hides PostgreSQL-specific behaviour. Use the same database engine and version as production.
Why does the manifest storage break tests?
It needs collectstatic output. Use plain StaticFilesStorage in test settings.
How do I reproduce a CI-only failure locally?
Run with the same environment variables, --shuffle with the CI seed, and the same Python and PostgreSQL versions.