# Use a fast password hasher in tests

> Django hashes passwords with hundreds of thousands of PBKDF2 iterations, and every create_user and client.login in a test pays for it. Switch to a fast hasher in test settings and use force_login.

Password hashing is slow on purpose: it makes stolen password hashes expensive to crack. Django's default hasher, PBKDF2, runs hundreds of thousands of SHA-256 iterations for every password, and the count goes up with every Django release. That takes tens to hundreds of milliseconds of CPU.

In production that cost is paid once per login. In a test suite it's paid every time a test creates a user with a password or logs one in, and in suites with many user-facing tests it's often the single biggest cost: a few hundred tests that each create and log in a user can spend minutes hashing.

## Why tests hash so many passwords

Each of these hashes a password:

- `User.objects.create_user(username, password=...)` and `create_superuser()`
- `user.set_password(...)` and `make_password(...)`
- `self.client.login(username=..., password=...)`, which runs `authenticate()` and checks the password against its hash
- factories that call `set_password` in a post-generation hook, e.g. factory_boy's `PostGenerationMethodCall("set_password", ...)`
- loading fixtures through a signup form or API

A test that creates a user and logs them in pays for two hashes. A `setUp` that does that for 50 tests pays for 100.

## The fix: a fast hasher in test settings

Use a cheap hasher only while testing. Django's own documentation recommends `MD5PasswordHasher` for this:

```python
# settings.py
import sys

TESTING = "test" in sys.argv or "pytest" in sys.modules

if TESTING:
    PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
```

If you keep separate test settings (`DJANGO_SETTINGS_MODULE=myproject.settings.test`), set it there instead.

## Log in without a password: force_login

`client.login()` checks the password, which means hashing it again. Unless the test is about the login form or authentication itself, use `force_login()`, which attaches the user to the session directly:

```python
# Before: hashes the password twice (create_user and login)
user = User.objects.create_user("alice", password="secret")
self.client.login(username="alice", password="secret")

# After: no hashing at all
user = User.objects.create_user("alice")
self.client.force_login(user)
```

`create_user()` without a password sets an unusable password and skips hashing. With pytest-django the `admin_client` fixture and `client.force_login(user)` work the same way.

## Outside Django

The same idea applies anywhere: use the cheapest work factor your hashing library allows, only in tests.

```python
# bcrypt: 4 is the minimum cost (the default is 12, which is 256 times slower)
bcrypt.hashpw(password, bcrypt.gensalt(rounds=4))

# argon2-cffi
from argon2 import PasswordHasher

hasher = PasswordHasher(time_cost=1, memory_cost=8, parallelism=1)

# passlib
pwd_context = CryptContext(schemes=["bcrypt"], bcrypt__rounds=4)
```

Make the parameters configurable (a setting or environment variable) so production keeps the strong defaults.

## Keep the tests correct

- Change the hasher in test settings only. A fast hasher in production would make stolen hashes easy to crack.
- Tests that assert on a specific hash format (`pbkdf2_sha256$...`) need updating, or an `override_settings(PASSWORD_HASHERS=[...])` on that one test.
- Keep a few tests that go through the real login form with `client.login()` or a POST, so authentication itself stays covered.

## How flakytest finds it

flakytest attributes the time spent in hashing functions (PBKDF2, bcrypt, argon2, scrypt) to a *password hashing* category. When it's more than about 1% of test time it suggests **Use a fast password hasher in tests**, shows your current hasher, and lists the lines that call `client.login()` or `authenticate()` so you know where `force_login()` would help.

## Further reading

- [Django: password hashing in tests](https://docs.djangoproject.com/en/stable/topics/testing/overview/#password-hashing)
- [Django: `Client.force_login()`](https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.Client.force_login)
