# Use an in-memory cache in tests

> Tests that talk to a real Redis or Memcached pay a network round trip per cache call and share state between tests. Use Django's LocMemCache or fakeredis in tests and clear it per test.

When test settings point the cache at a real Redis or Memcached server, every `cache.get()` and `cache.set()` in the code under test is a network call: send the key and value over a socket, wait for the server, read the reply. Each one is fast on its own, but code that caches per object or per request makes many of them, and a test suite runs that code thousands of times.

A real cache server also makes tests less reliable. It keeps state between tests (and between runs), so a value cached by one test changes what the next test sees, and [pytest-xdist](https://flakytest.dev/guides/pytest-xdist/) workers sharing one Redis database step on each other's keys.

## Why a real cache server is slow in tests

- **A round trip per call.** Even on localhost, a call goes through the kernel's network stack twice. In CI the server is often a service container, and a `get_many` loop or a cache-per-row pattern turns into hundreds of round trips per test.
- **Connections.** Clients connect lazily, and code that creates a new client per call reconnects every time.

An in-memory cache keeps values in a Python dict in the test process. There's no socket, and nothing survives the process.

## Django: LocMemCache in test settings

```python
# settings/test.py
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
    },
}
```

If your settings define several caches (`"default"`, `"sessions"`, `"ratelimit"`), override each of them. Give each one a different `LOCATION` so they stay separate:

```python
CACHES = {
    "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "default"},
    "sessions": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "sessions"},
}
```

`LocMemCache` supports the whole Django cache API (`get`, `set`, `get_many`, `incr`, `touch`, timeouts), and each pytest-xdist worker gets its own.

`DummyCache` is even cheaper, but it never stores anything: `cache.get()` always returns `None`. Only use it if no test depends on a value being cached.

## When you need Redis features: fakeredis

Code that uses Redis beyond the Django cache API (django-redis's `cache.lock()` or `ttl()`, sorted sets, raw `redis` client calls) needs something that speaks Redis. fakeredis implements the Redis commands in Python, in memory.

With Django's built-in Redis backend:

```python
# settings/test.py
from fakeredis import FakeConnection

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://localhost:6379",
        "OPTIONS": {"connection_class": FakeConnection},
    },
}
```

With django-redis, pass it through the connection pool instead:

```python
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://localhost:6379/0",
        "OPTIONS": {"CONNECTION_POOL_KWARGS": {"connection_class": FakeConnection}},
    },
}
```

Outside Django, inject the client and give tests a fake one:

```python
import fakeredis
import pytest

from myapp.cache import RateLimiter


@pytest.fixture
def redis_client():
    client = fakeredis.FakeRedis()
    yield client
    client.flushall()


def test_rate_limit(redis_client):
    limiter = RateLimiter(redis_client, limit=2)
    assert limiter.allow("alice")
    assert limiter.allow("alice")
    assert not limiter.allow("alice")
```

For asyncio code, use `fakeredis.FakeAsyncRedis`. To make several clients share data (an app client and a test client), pass them the same `fakeredis.FakeServer()`.

## Clear the cache between tests

An in-memory cache still lives as long as the test process, so a value set in one test is there in the next. Clear it in an autouse fixture:

```python
# conftest.py
import pytest
from django.core.cache import caches


@pytest.fixture(autouse=True)
def clear_caches():
    yield
    for cache in caches.all():
        cache.clear()
```

With `unittest`-style Django tests, do the same in `setUp` of a shared base class or in a `tearDown`. Clearing an in-memory cache is cheap, so doing it for every test costs next to nothing.

## Keep the tests correct

- Clear the cache between tests (`cache.clear()` in an autouse fixture) so [tests stay independent](https://flakytest.dev/guides/test-isolation/).
- Keep a few tests against a real server for code that depends on its exact behaviour: eviction, Lua scripts, cluster or replication, persistence. Mark them (for example `@pytest.mark.redis`) and run them in a CI job that has a Redis service.
- `LocMemCache` has no key length or character limits, while Memcached rejects keys over 250 characters or with spaces. Django warns about such keys (`CacheKeyWarning`) with every backend; turn that warning into an error in tests if you deploy on Memcached.

## How flakytest finds it

flakytest puts time spent in Redis and Memcached clients and Django cache backends into a *cache* category. When that adds up to at least 0.5 seconds and 1% of test time, it suggests **Use an in-memory cache in tests**. If your Django settings use a Redis or Memcached backend, the suggestion shows the `LocMemCache` setting to use, and it lists the lines of your code that make the cache calls.

## Further reading

- [Django: local-memory caching](https://docs.djangoproject.com/en/stable/topics/cache/#local-memory-caching)
- [Django: dummy caching](https://docs.djangoproject.com/en/stable/topics/cache/#dummy-caching-for-development)
- [fakeredis documentation](https://fakeredis.readthedocs.io/en/latest/)
