# Testing

> pytest against real, isolated infrastructure

Encore.py apps are tested with [pytest](https://docs.pytest.org), run through `encore test`. Encore gives the test process its own databases, buckets, Pub/Sub topics and Redis keys, created when the tests first use them and removed when the process exits. Your tests use the same declarations and the same SDK calls as your service, against real Postgres and real emulators, so most tests of Encore code are integration tests.

## Running tests

Run all tests from anywhere in the app:

```shell
encore test
```

For each Python service, `encore test` runs `pytest` in the service's directory, through the package manager detected from its lockfile: `uv run pytest` with a `uv.lock`, `poetry run pytest` with a `poetry.lock`, and so on, or `python3 -m pytest` without one. With several services, it tests them in parallel and prefixes each line of output with the service's name. To change the command, set a custom `test` in [`encore.service.json`](/docs/py/primitives/services#running-and-testing-a-service).

To choose which services to test, pass a service-set expression with `--services`, and limit how many run at once with `--concurrency`:

```shell
encore test --services orders
encore test --services 'orders | billing' --concurrency 1
```

To pass arguments to pytest, give the whole command after `encore test`. Without `--services`, the command runs as given, in the current directory; with `--services`, it runs in each selected service's directory:

```shell
# From the service's directory:
encore test uv run pytest -k create_order -x

# In the orders service's directory, wherever you are in the app:
encore test --services orders uv run pytest tests/test_orders.py
```

Always run tests through `encore test`. Importing `encore` initializes the SDK's runtime, which gets its configuration from Encore; running `pytest` on its own fails at that import.

## Setting up pytest

Add pytest to the service's development dependencies, together with [pytest-asyncio](https://pytest-asyncio.readthedocs.io) for testing async code. Most SDK methods are `async`, so most tests are too:

```toml
-- pyproject.toml --
[dependency-groups]
dev = ["pytest>=8", "pytest-asyncio>=0.24"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
```

`pythonpath = ["."]` lets tests in `tests/` import the service's modules, such as `from resources import db`, the same way the service does.

Mark async tests with `@pytest.mark.asyncio`, and declare async fixtures with `@pytest_asyncio.fixture`. To skip the markers, set `asyncio_mode = "auto"` in `[tool.pytest.ini_options]`, and pytest-asyncio treats every `async def` test and fixture as asyncio.

## Testing database code

Test code queries the database through its declaration, as the service does:

```python
-- tests/test_users.py --
from uuid import uuid4

import pytest

from resources import db


@pytest.mark.asyncio
async def test_add_user() -> None:
    name = f"user-{uuid4().hex}"
    await db.exec("INSERT INTO users (name) VALUES ($1)", name)

    row = await db.query("SELECT name FROM users WHERE name = $1", name).one()
    assert row == {"name": name}
```

Each test process gets its own copy of each database, with every migration applied, including rows that migrations insert. Encore migrates a template database once and clones it for each test process, so starting from a fresh database is fast. The template is reused across runs until the migrations change.

## What Encore sets up

Nothing is created until a test uses it. On first use in a test process, Encore creates an instance of the resource for that process:

| Resource | Isolation |
|---|---|
| [Database](/docs/py/primitives/databases) | A clone of the migrated template database. |
| [Bucket](/docs/py/primitives/object-storage) | A key prefix of its own in the local object storage. |
| [Pub/Sub topic and subscription](/docs/py/primitives/pubsub) | Names of their own in the local broker. |
| [Redis database](/docs/py/primitives/redis) | A namespace of its own on the local Redis server. |

When the test process exits, Encore removes everything it created for it. Test databases run on a separate Postgres cluster, configured without durability (`fsync` is off) since tests don't need it. Encore keeps the cluster running for a while after a test run, so the next run starts quickly.

During `encore test`, the environment type is `"test"`, which [`encore.app_meta()`](/docs/py/develop/metadata) reports as `environment.type`.

## Keeping tests independent

Isolation is per test process, not per test: every test in one pytest run shares the same database, bucket, topics and Redis namespace. Encore.py doesn't yet have per-test databases or fixtures for saved test data.

So write tests that don't depend on each other's data:

- Use unique keys, names and IDs in each test, for example with `uuid4()`.
- Clean up in a fixture, so the cleanup also runs when a test fails.
- Don't assert on totals across a table, such as `count(*)`, that other tests also write to. Filter on the rows the test created.

This fixture gives each object storage test a key of its own, and removes the object afterwards:

```python
-- tests/test_objects.py --
from contextlib import suppress
from uuid import uuid4

import pytest
import pytest_asyncio
from encore.storage import objects

from resources import uploads


@pytest_asyncio.fixture
async def object_key():
    key = f"tests/{uuid4().hex}.txt"
    yield key
    with suppress(objects.ObjectNotFound):
        await uploads.remove(key)


@pytest.mark.asyncio
async def test_upload_and_download(object_key: str) -> None:
    attrs = await uploads.upload(object_key, b"abcdef", content_type="text/plain")
    assert attrs.size == 6
    assert await uploads.download(object_key) == b"abcdef"
```

Separate test processes are isolated from each other, so services tested in parallel by `encore test`, or pytest workers in separate processes, don't see each other's data.

With [Hypothesis](https://hypothesis.readthedocs.io), a function-scoped fixture runs once per test, not once per generated example. Create the key inside the test instead, for example with an async context manager that cleans up after each example.

## Testing Pub/Sub subscriptions

Subscription handlers only consume messages once `encore.start()` has been awaited, and a test process doesn't call it for you. To test a handler's logic, call the handler function directly.

## Testing Django apps

Django tests run under `encore test` like any other pytest suite. Configure Django before the first test, for example by importing the WSGI application, and apply Django's migrations to the test database yourself, since Encore only applies Encore-managed migrations:

```python
-- tests/test_orm.py --
from uuid import uuid4

import pytest
from django.core.management import call_command
from django.db import connections
from django.test import Client

from config.wsgi import application  # noqa: F401  (configures Django)
from storage.models import Note


@pytest.fixture(scope="module")
def migrated_database():
    # Encore supplies the isolated database; Django owns its schema.
    call_command("migrate", interactive=False, verbosity=0)
    yield
    connections.close_all()


@pytest.fixture
def note_title(migrated_database):
    title = f"note-{uuid4().hex}"
    yield title
    Note.objects.filter(title=title).delete()


def test_create_note(note_title):
    response = Client().post("/notes/", {"title": note_title})
    assert response.status_code == 201
    assert Note.objects.get(pk=response.json()["id"]).title == note_title
```

The database Encore provides is already isolated, so this uses plain pytest and Django's test `Client`, not Django's test runner, which would create a test database of its own. Synchronous tests call the SDK's async methods through `asgiref.sync.async_to_sync`, as synchronous Django views do:

```python
from asgiref.sync import async_to_sync

from storage.resources import db


def test_note_row(note_title):
    note = Note.objects.create(title=note_title)
    row = async_to_sync(
        db.query("SELECT title FROM storage_note WHERE id = $1", note.pk).one
    )()
    assert row == {"title": note_title}
```
