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.

What CI does not inheritYour laptopdb, .env, staticCI runnerclean every timeHidden assumptionbreaks in CIMake it explicitservices, env, setupEvery CI failure points at something your machine provides implicitly.
  1. Run PostgreSQL as a service with a health check, and set DATABASE_URL to it.
  2. Give the test role permission to create databases (CREATEDB), since Django creates test_<name>.
  3. Provide required settings as CI variables or a test settings module; never depend on a local .env.
  4. Use StaticFilesStorage (not the manifest storage) in test settings, or run collectstatic first.
  5. Pin TIME_ZONE and use freezegun or time_machine for date-dependent tests.
  6. Reproduce order bugs locally with --shuffle and --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.

CI error to missing assumptionCI errorCause and fixcould not connect to serverservice not ready orwrong host; health checkpermission denied to create databasetest role needs CREATEDBSECRET_KEY must not be emptyno .env in CI; set variablesMissing staticfiles manifest entrymanifest storage intests; use plain storagepasses alone, fails in suiteshared state or order;--shuffle locallyEach error maps to something your laptop provides without being askeddjango-deployment.com
Fix the environment, not the test, when the error is about setup.

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.

a CI job that matches realityPostgreSQL service with health checksame major version as prodRole can create the test databaseCREATEDBTest settings module in the repono reliance on .envPlain static storage in testsno manifest neededTime frozen where dates matterrunner is UTCTesting against PostgreSQL in CI also catches bugs SQLite hidesdjango-deployment.com
Match the production database version so CI catches version-specific behaviour.

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 needs CREATEDB for all of them.
  • Caching dependencies with actions/setup-python's cache: pip speeds up runs significantly.
  • Docker-based CI (running tests inside your production image) catches missing system libraries early.

Next steps

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.

Keep going