Python · Тестирование с pytest · Экспертный

Тестирование Django-приложений с pytest-django

5 задач

Тестируйте Django views, модели и формы с помощью встроенных фикстур pytest-django.

Тестирование Django-приложений с pytest-django

#
pytest-django соединяет экосистему pytest с тестовой инфраструктурой Django. Он даёт вам все возможности pytest -- фикстуры, parametrize, марки, плагины -- при этом управляя базой данных Django, тестовым клиентом и настройками. ## Установка и настройка ```bash pip install pytest-django ``` Укажите pytest расположение настроек Django, добавив в `pytest.ini` в корне проекта: ```ini [pytest] DJANGO_SETTINGS_MODULE = myproject.settings ``` Без этой строки pytest-django не сможет инициализировать Django, и любой импорт из ваших моделей вызовет `django.core.exceptions.AppRegistryNotReady`. ## Доступ к базе данных отключён по умолчанию Важнейшее правило pytest-django: **доступ к базе данных отключён по умолчанию.** Любой тест, пытающийся обратиться к базе данных, вызовет ошибку, если только вы явно не разрешили доступ. Это сделано намеренно -- тесты, которым не нужна база данных, никогда не должны платить её цену или рисковать побочными эффектами. Включите доступ для конкретного теста с помощью маркера `@pytest.mark.django_db`: ```python import pytest from myapp.models import Task @pytest.mark.django_db def test_task_creation(): task = Task.objects.create(title='Buy milk') assert Task.objects.count() == 1 assert task.title == 'Buy milk' ``` ## Изоляция транзакций Каждый тест с `@pytest.mark.django_db` выполняется внутри транзакции базы данных, которая **откатывается** после завершения теста. Каждый тест начинается с чистым состоянием и не может загрязнить другие тесты оставшимися строками: ```python @pytest.mark.django_db def test_a(): Task.objects.create(title='Task from test_a') assert Task.objects.count() == 1 @pytest.mark.django_db def test_b(): assert Task.objects.count() == 0 # строка test_a была откатана ``` Для тестов, которым нужны **реально** зафиксированные транзакции -- код с хуками `on_commit()`, `select_for_update()` или чтением базы данных из другого процесса -- используйте `@pytest.mark.django_db(transaction=True)`. Этот режим сбрасывает базу данных после каждого теста вместо отката, что медленнее, но точнее для кода, чувствительного к транзакциям. ## Ключевые встроенные фикстуры pytest-django предоставляет несколько фикстур автоматически, внедряемых по имени параметра: **`client`** -- Django `TestClient`, отправляющий HTTP-запросы через весь стек Django (маршрутизация, промежуточное ПО, представления) без запуска реального сервера: ```python @pytest.mark.django_db def test_task_list(client): response = client.get('/tasks/') assert response.status_code == 200 ``` **`rf`** -- `RequestFactory`, строящий объекты `HttpRequest` напрямую, минуя URL-маршрутизацию и всё промежуточное ПО. Используйте его для тестирования функции представления в полной изоляции: ```python from myapp.views import task_list @pytest.mark.django_db def test_task_list_view(rf): request = rf.get('/tasks/') response = task_list(request) assert response.status_code == 200 ``` **`admin_client`** -- как `client`, но предварительно аутентифицированный как суперпользователь Django. Полезен для тестирования административных представлений без ручного создания учётных данных. **`settings`** -- позволяет переопределить любую настройку Django на время одного теста. Изменения автоматически возвращаются после завершения теста: ```python def test_custom_cache(settings): settings.CACHES = { 'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'} } from django.core.cache import cache cache.set('key', 'value', 30) assert cache.get('key') == 'value' ``` **`django_user_model`** -- возвращает класс модели пользователя, настроенный в `AUTH_USER_MODEL`. Всегда предпочитайте его прямому импорту `User`, чтобы ваши тесты работали с кастомными моделями пользователей: ```python @pytest.mark.django_db def test_user_creation(django_user_model): user = django_user_model.objects.create_user(username='alice', password='x') assert user.username == 'alice' ``` ## Предоставление доступа к БД фикстурам Когда фикстуре нужно обратиться к базе данных, добавьте встроенную фикстуру `db` как параметр. Это эквивалент `@pytest.mark.django_db` на уровне фикстуры: ```python @pytest.fixture def sample_task(db): from myapp.models import Task return Task.objects.create(title='Fixture task') @pytest.mark.django_db def test_task_is_incomplete(sample_task): assert sample_task.completed is False ``` Если сам тест несёт `@pytest.mark.django_db`, его фикстуры автоматически наследуют доступ к базе данных -- не нужно добавлять `db` в каждую фикстуру. Параметр `db` нужен главным образом когда фикстура находится в `conftest.py` и используется в нескольких тестовых файлах, где некоторые тесты могут не нести маркер. ## Тестирование email с `mailoutbox` Фикстура `mailoutbox` перехватывает все исходящие письма Django, отправленные во время теста. Она автоматически переключает бэкенд электронной почты на in-memory бэкенд `locmem` Django -- ручное переопределение `settings.EMAIL_BACKEND` не нужно: ```python import pytest @pytest.mark.django_db def test_welcome_email_is_sent(mailoutbox, django_user_model): django_user_model.objects.create_user(username='alice', password='x') assert len(mailoutbox) == 1 msg = mailoutbox[0] assert msg.subject == 'Welcome, alice!' assert '[email protected]' in msg.to ``` `mailoutbox` -- список объектов `django.core.mail.EmailMessage`. Каждое сообщение предоставляет `subject`, `body`, `from_email`, `to`, `cc`, `bcc` и `attachments`. Список автоматически очищается перед каждым тестом -- ручной `mail.outbox.clear()` не нужен. Альтернатива -- установить `settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'` в тесте и читать `django.core.mail.outbox` напрямую. Оба способа работают, но `mailoutbox` -- идиоматический pytest-django: никаких импортов, никакой очистки и гарантированная изоляция между тестами. ## Маркер на уровне класса с `pytestmark` Повторять `@pytest.mark.django_db` на каждом методе тестового класса -- избыточно. Используйте `pytestmark` для применения маркера к целому классу или модулю: ```python import pytest from myapp.models import Task class TestTaskModel: pytestmark = pytest.mark.django_db # применяется к каждому методу в этом классе def test_creation(self): task = Task.objects.create(title='Buy milk') assert Task.objects.count() == 1 def test_default_completed(self): task = Task.objects.create(title='New task') assert task.completed is False ``` На уровне модуля `pytestmark` помечает каждую тестовую функцию в файле: ```python # tests/test_models.py import pytest pytestmark = pytest.mark.django_db # каждая функция в этом модуле получает доступ к БД ``` `pytestmark` -- стандартное соглашение pytest, а не специфика pytest-django. Работает с любым маркером и любым уровнем, подходящим для вашей организации кода -- `pytestmark` на уровне модуля часто используется в выделенных файлах тестов моделей. ## Жизненный цикл тестовой БД и `--reuse-db` По умолчанию pytest-django создаёт свежую тестовую базу данных в начале каждого запуска `pytest` и уничтожает её по завершении. Для проектов с большим количеством миграций это добавляет значительное время старта при каждом запуске. Флаг `--reuse-db` сохраняет тестовую базу данных между запусками. При последующих запусках pytest-django проверяет наличие непримерённых миграций и применяет только новые: ```bash pytest --reuse-db # первый запуск: создаёт БД; последующие: переиспользуют pytest --create-db # принудительное пересоздание независимо от текущего состояния ``` Типичная настройка для крупных проектов: ```ini [pytest] DJANGO_SETTINGS_MODULE = myproject.settings addopts = --reuse-db ``` Когда вы знаете, что схема изменилась фундаментально (миграции объединены, переименованы): ```bash pytest --create-db ``` `--reuse-db` безопасен с дефолтным режимом `transaction=False`, поскольку каждый тест по-прежнему откатывает свои изменения внутри транзакции базы данных. Постоянная база данных хранит только схему и seed-данные, загруженные в начале сессии -- тесты не могут постоянно записывать строки в неё. **Исключение:** тесты, помеченные `transaction=True`, фиксируют данные в базе данных. В сочетании с `--reuse-db` зафиксированные строки из одной сессии переживают следующую. Либо избегайте сочетания `transaction=True` с `--reuse-db`, либо добавьте фикстуру завершения уровня session, удаляющую зафиксированные строки. ## `live_server` -- тесты с реальным HTTP-сервером Фикстура `live_server` запускает реальный WSGI-сервер на случайном порту и возвращает объект, атрибут `.url` которого является базовым URL сервера: ```python @pytest.mark.django_db(transaction=True) def test_homepage_over_http(live_server): import urllib.request resp = urllib.request.urlopen(f'{live_server.url}/') assert resp.status == 200 ``` `live_server` всегда требует `transaction=True`. Сервер работает в отдельном потоке -- без реально зафиксированных строк поток сервера не видит данные, созданные в откатанной транзакции потока теста. Используйте `live_server` для тестов с автоматизацией браузера (Selenium, Playwright) или когда нужно проверить что полный стек WSGI (сервер, промежуточное ПО, представления) работает от начала до конца через реальное TCP-соединение. Для всех прочих тестов представлений и API фикстура `client` (запускающая приложение в процессе) проще и быстрее.

Паттерны тестирования Django на практике

#
## Тестируемое приложение Все примеры используют небольшое Django-приложение со следующей структурой: ```python # myapp/models.py from django.db import models from django.contrib.auth.models import User class Task(models.Model): title = models.CharField(max_length=200) completed = models.BooleanField(default=False) owner = models.ForeignKey( User, on_delete=models.CASCADE, null=True, blank=True ) ``` ```python # myapp/views.py from django.http import JsonResponse from django.contrib.auth.decorators import login_required from .models import Task def task_list(request): tasks = list(Task.objects.values('id', 'title', 'completed')) return JsonResponse({'results': tasks}) @login_required def my_tasks(request): tasks = list(Task.objects.filter(owner=request.user).values('id', 'title')) return JsonResponse({'results': tasks}) ``` ```python # myapp/urls.py from django.urls import path from . import views urlpatterns = [ path('tasks/', views.task_list), path('my-tasks/', views.my_tasks), ] ``` ## Тестирование представлений с `client` ```python import pytest @pytest.mark.django_db def test_task_list_returns_200(client): response = client.get('/tasks/') assert response.status_code == 200 data = response.json() assert 'results' in data assert isinstance(data['results'], list) @pytest.mark.django_db def test_task_list_shows_created_tasks(client): from myapp.models import Task Task.objects.create(title='Buy groceries') Task.objects.create(title='Write tests') response = client.get('/tasks/') results = response.json()['results'] assert len(results) == 2 titles = [t['title'] for t in results] assert 'Buy groceries' in titles ``` ## Тестирование сохранения и извлечения из модели После вызова `.save()` или `.create()` всегда получайте свежий экземпляр из базы данных для проверки, что значение реально сохранилось, а не просто хранится в памяти: ```python @pytest.mark.django_db def test_task_default_completed_is_false(): from myapp.models import Task task = Task.objects.create(title='New task') task_from_db = Task.objects.get(pk=task.pk) assert task_from_db.completed is False @pytest.mark.django_db def test_task_can_be_marked_complete(): from myapp.models import Task task = Task.objects.create(title='Finish report', completed=False) task.completed = True task.save() task.refresh_from_db() assert task.completed is True ``` `refresh_from_db()` перезагружает все поля из базы данных на существующем экземпляре на месте -- немного чище, чем `Task.objects.get(pk=task.pk)`, когда объект уже есть. ## Аутентифицированные представления с `force_login` `client.force_login(user)` обходит проверку пароля и напрямую помечает сессию как аутентифицированную. Предпочтительнее `client.login()` в тестах, поскольку не привязывает тест к хешированию паролей или настройке бэкенда аутентификации: ```python @pytest.mark.django_db def test_my_tasks_redirects_anonymous(client): response = client.get('/my-tasks/') assert response.status_code == 302 # редирект на страницу входа @pytest.mark.django_db def test_my_tasks_returns_only_owners_tasks(client, django_user_model): alice = django_user_model.objects.create_user(username='alice', password='x') bob = django_user_model.objects.create_user(username='bob', password='x') from myapp.models import Task Task.objects.create(title="Alice's task", owner=alice) Task.objects.create(title="Bob's task", owner=bob) client.force_login(alice) response = client.get('/my-tasks/') assert response.status_code == 200 results = response.json()['results'] assert len(results) == 1 assert results[0]['title'] == "Alice's task" ``` ## Переопределение настроек с фикстурой `settings` ```python @pytest.mark.django_db def test_welcome_email_is_sent(settings, django_user_model): settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' from django.core import mail # предполагается что сигнал создания пользователя отправляет приветственное письмо django_user_model.objects.create_user(username='alice', password='x') assert len(mail.outbox) == 1 assert 'Welcome' in mail.outbox[0].subject ``` Фикстура `settings` возвращает каждый установленный атрибут независимо от того, прошёл тест или упал. Поэтому она всегда предпочтительнее патчинга `django.conf.settings` через `unittest.mock.patch`. Для тестирования email конкретно фикстура `mailoutbox` из MAT1 -- более чистая альтернатива: она автоматически настраивает локальный бэкенд и очищает ящик между тестами без ручной настройки `settings.EMAIL_BACKEND`. ## Тестирование с `rf` (RequestFactory) `rf` строит объекты `HttpRequest`, не проходя через URL-маршрутизацию и промежуточное ПО. Тест вызывает функцию представления напрямую: ```python from myapp.views import task_list @pytest.mark.django_db def test_task_list_with_rf(rf): from myapp.models import Task Task.objects.create(title='RF task') request = rf.get('/tasks/') response = task_list(request) assert response.status_code == 200 ``` Поскольку `rf` обходит URL-маршрутизацию и всё промежуточное ПО запросов -- включая аутентификацию и сессионное ПО -- `request.user` не заполняется из сессии и по умолчанию равен `AnonymousUser`. **Декораторы представлений вроде `@login_required` по-прежнему выполняются** как часть вызова представления: с анонимным пользователем декоратор делает редирект. Назначьте `request.user` аутентифицированному объекту пользователя перед вызовом любого представления, проверяющего его: ```python @pytest.mark.django_db def test_my_tasks_with_rf(rf, django_user_model): from myapp.views import my_tasks from myapp.models import Task user = django_user_model.objects.create_user(username='alice', password='x') Task.objects.create(title="Alice's task", owner=user) request = rf.get('/my-tasks/') request.user = user response = my_tasks(request) assert response.status_code == 200 ``` Компромисс: тесты с `rf` быстрее и тестируют только логику представления. Тесты с `client` медленнее, но проверяют что маршрутизация, промежуточное ПО и аутентификация работают вместе. Используйте `rf` для юнит-тестирования функций представлений, `client` -- для интеграционного тестирования полного цикла запроса. ## Тестирование Django REST Framework API с `APIClient` Если проект использует DRF, используйте `APIClient` из `rest_framework.test` вместо Django `TestClient`. `APIClient` понимает согласование контента DRF, `response.data` и слои аутентификации: ```python import pytest from rest_framework.test import APIClient @pytest.fixture def api_client(): return APIClient() @pytest.mark.django_db def test_task_list_returns_empty(api_client): response = api_client.get('/api/tasks/') assert response.status_code == 200 assert response.data['results'] == [] # .data уже разобран -- .json() не нужен ``` `response.data` -- разобранный Python-объект, произведённый сериализатором DRF: dict, list или `OrderedDict`. Эквивалентен вызову `response.json()`, но отражает вывод сериализатора DRF напрямую, включая вложенные отношения. Для аутентификации используйте `force_authenticate()` вместо `force_login()`: ```python @pytest.mark.django_db def test_create_task_as_alice(api_client, django_user_model): user = django_user_model.objects.create_user(username='alice', password='x') api_client.force_authenticate(user=user) response = api_client.post('/api/tasks/', {'title': 'Deploy fix'}, format='json') assert response.status_code == 201 assert response.data['title'] == 'Deploy fix' ``` `force_authenticate(user=user)` устанавливает пользователя напрямую на уровне разрешений DRF -- без сессии, поиска токена и проверки пароля. Используйте для тестирования логики представлений и сериализаторов в изоляции от задач аутентификации. `force_login()` (на Django `TestClient`) работает на уровне сессии и подходит, когда тест специально нуждается в cookie сессии. Для API DRF, защищённых токеном или JWT, `force_authenticate()` всегда проще и быстрее.

Справочник pytest-django

#
## Настройка pytest.ini ```ini [pytest] DJANGO_SETTINGS_MODULE = myproject.settings testpaths = tests addopts = -v --tb=short ``` ## Встроенные фикстуры | Фикстура | Тип | Назначение | |---|---|---| | `client` | `TestClient` | HTTP полного стека -- маршрутизация, промежуточное ПО, представления | | `admin_client` | `TestClient` | Предаутентифицирован как суперпользователь Django | | `rf` | `RequestFactory` | Только объекты запроса -- без маршрутизации и промежуточного ПО | | `db` | fixture | Предоставить доступ к БД телу фикстуры | | `settings` | `UserSettingsHolder` | Переопределить настройки Django; авто-возврат после теста | | `django_user_model` | класс модели | Учитывает `AUTH_USER_MODEL`; работает с кастомными пользователями | | `mailoutbox` | list | Перехватывает исходящие письма; авто-настраивает locmem для теста | | `live_server` | LiveServer | Запускает реальный WSGI-сервер; URL через `live_server.url`; для Selenium/Playwright | | `django_db_setup` | session fixture | Управляет созданием/удалением БД на уровне сессии | ## Опции `@pytest.mark.django_db` | Опция | По умолчанию | Примечания | |---|---|---| | `transaction=False` | да | Оборачивает тест в откатываемую транзакцию (быстро) | | `transaction=True` | | Сбрасывает БД после теста; нужно для `on_commit`, `select_for_update` | | `reset_sequences=True` | | Сбрасывает авто-инкрементные ID; только с `transaction=True` | | `databases` | `['default']` | Список псевдонимов БД, к которым тест может обращаться | ## Паттерны аутентификации ```python # Предпочтительно в тестах -- обходит проверку пароля client.force_login(user) # Тоже работает -- проверяет учётные данные через бэкенд аутентификации client.login(username='alice', password='secret') # Для rf: установить request.user вручную (промежуточное ПО не установит его) request.user = user ``` ## Паттерны доступа фикстур к БД ```python # Вариант A -- маркер на уровне теста (фикстуры наследуют доступ) @pytest.mark.django_db def test_something(): MyModel.objects.create(...) # Вариант B -- фикстура явно запрашивает db @pytest.fixture def my_obj(db): return MyModel.objects.create(...) # Вариант C -- transactional_db для тестов с transaction=True @pytest.fixture def my_obj(transactional_db): return MyModel.objects.create(...) ``` ## Распространённые паттерны ```python # Проверить персистентность (а не только состояние в памяти) obj.refresh_from_db() Task.objects.get(pk=task.pk) # POST с JSON-телом client.post('/api/', data={'key': 'val'}, content_type='application/json') # Получить данные ответа (Django TestClient) data = response.json() assert data['key'] == 'value' # DRF APIClient: используйте response.data (разобрано сериализатором, .json() не нужен) # assert response.data['key'] == 'value' # Проверить адрес редиректа assert response.status_code == 302 assert response['Location'] == '/login/' ```
01

Проверить что Django-представление возвращает HTTP 200

#

У вас есть Django-представление `task_list` по URL `/tasks/`, возвращающее JSON-ответ с ключом `results`, содержащим список задач. Настройте `pytest.ini` с `DJANGO_SETTINGS_MODULE` и напишите тест, использующий встроенную фикстуру `client`, который: 1. Вызывает `GET /tasks/` 2. Проверяет что статус-код равен 200 3. Проверяет что JSON ответа содержит ключ `results` 4. Проверяет что `results` является списком

# pytest.ini
# [pytest]
# DJANGO_SETTINGS_MODULE = myproject.settings

# tests/test_views.py
import pytest


@pytest.mark.django_db
def test_task_list_returns_200(client):
    # вызвать GET /tasks/ и проверить статус + структуру
    pass
Решение
# pytest.ini
# [pytest]
# DJANGO_SETTINGS_MODULE = myproject.settings

# tests/test_views.py
import pytest


@pytest.mark.django_db
def test_task_list_returns_200(client):
    response = client.get('/tasks/')
    assert response.status_code == 200
    data = response.json()
    assert 'results' in data
    assert isinstance(data['results'], list)
02

Проверить сохранение модели и персистентность в базе данных

#

У вас есть модель `Task` с полями `title` (CharField) и `completed` (BooleanField, default=False). Напишите два теста: 1. **test_default_completed_is_false** -- создайте `Task`, затем получите его из базы данных по первичному ключу и проверьте что `completed` равен `False`. 2. **test_task_can_be_completed** -- создайте `Task` с `completed=False`, установите `completed=True`, вызовите `.save()`, затем вызовите `.refresh_from_db()` и проверьте что значение сохранилось.

import pytest
from myapp.models import Task


@pytest.mark.django_db
def test_default_completed_is_false():
    # создать и получить свежий из БД
    pass


@pytest.mark.django_db
def test_task_can_be_completed():
    # создать, обновить, сохранить, обновить из БД, проверить
    pass
Решение
import pytest
from myapp.models import Task


@pytest.mark.django_db
def test_default_completed_is_false():
    task = Task.objects.create(title='New task')
    task_from_db = Task.objects.get(pk=task.pk)
    assert task_from_db.completed is False


@pytest.mark.django_db
def test_task_can_be_completed():
    task = Task.objects.create(title='Finish report', completed=False)
    task.completed = True
    task.save()
    task.refresh_from_db()
    assert task.completed is True
03

Проверить аутентифицированное представление с force_login

#

У вас есть представление `my_tasks` по `/my-tasks/`, декорированное `@login_required`. Оно возвращает только задачи, принадлежащие аутентифицированному пользователю, в формате JSON `{'results': [...]}`. Напишите два теста: 1. **test_my_tasks_redirects_anonymous** -- проверьте что неаутентифицированный GET возвращает 302. 2. **test_my_tasks_returns_only_owners_tasks** -- создайте двух пользователей и по одной задаче для каждого. Войдите как `user1` через `client.force_login()`. Проверьте статус 200 и что ответ содержит ровно одну задачу, принадлежащую `user1`.

import pytest
from myapp.models import Task


@pytest.mark.django_db
def test_my_tasks_redirects_anonymous(client):
    pass


@pytest.mark.django_db
def test_my_tasks_returns_only_owners_tasks(client, django_user_model):
    pass
Решение
import pytest
from myapp.models import Task


@pytest.mark.django_db
def test_my_tasks_redirects_anonymous(client):
    response = client.get('/my-tasks/')
    assert response.status_code == 302


@pytest.mark.django_db
def test_my_tasks_returns_only_owners_tasks(client, django_user_model):
    alice = django_user_model.objects.create_user(username='alice', password='x')
    bob = django_user_model.objects.create_user(username='bob', password='x')

    Task.objects.create(title="Alice's task", owner=alice)
    Task.objects.create(title="Bob's task", owner=bob)

    client.force_login(alice)
    response = client.get('/my-tasks/')
    assert response.status_code == 200
    results = response.json()['results']
    assert len(results) == 1
    assert results[0]['title'] == "Alice's task"
04

Переопределить настройку Django для одного теста с фикстурой settings

#

У вас есть вспомогательная функция: ```python # myapp/utils.py from django.conf import settings def get_max_tasks(): return getattr(settings, 'MAX_TASKS_PER_USER', 10) ``` Напишите два теста: 1. **test_get_max_tasks_with_override** -- используйте фикстуру `settings` для установки `MAX_TASKS_PER_USER = 3` и проверьте что `get_max_tasks()` возвращает `3`. 2. **test_get_max_tasks_default** -- не используйте фикстуру `settings`; проверьте что `get_max_tasks()` возвращает дефолтное значение `10`. Оба теста должны проходить независимо от порядка выполнения.

# tests/test_utils.py
import pytest
from myapp.utils import get_max_tasks


def test_get_max_tasks_with_override(settings):
    pass


def test_get_max_tasks_default():
    pass
Решение
# tests/test_utils.py
import pytest
from myapp.utils import get_max_tasks


def test_get_max_tasks_with_override(settings):
    settings.MAX_TASKS_PER_USER = 3
    assert get_max_tasks() == 3


def test_get_max_tasks_default():
    assert get_max_tasks() == 10
05

Проверить функцию представления напрямую с RequestFactory

#

У вас есть функция представления `task_list` (не URL -- сама функция). Напишите тест, использующий фикстуру `rf` (RequestFactory), который: 1. Создаёт два объекта `Task` в базе данных. 2. Строит GET-запрос через `rf.get('/tasks/')`. 3. Вызывает функцию представления напрямую: `response = task_list(request)`. 4. Проверяет статус 200 и что JSON-ответ содержит ровно 2 задачи. Затем напишите второй тест для аутентифицированного представления `my_tasks`. Поскольку `rf` пропускает промежуточное ПО, `request.user` не устанавливается автоматически -- назначьте его вручную.

import json
import pytest
from myapp.views import task_list, my_tasks
from myapp.models import Task


@pytest.mark.django_db
def test_task_list_with_rf(rf):
    pass


@pytest.mark.django_db
def test_my_tasks_with_rf(rf, django_user_model):
    pass
Решение
import json
import pytest
from myapp.views import task_list, my_tasks
from myapp.models import Task


@pytest.mark.django_db
def test_task_list_with_rf(rf):
    Task.objects.create(title='First')
    Task.objects.create(title='Second')

    request = rf.get('/tasks/')
    response = task_list(request)

    assert response.status_code == 200
    data = json.loads(response.content)
    assert len(data['results']) == 2


@pytest.mark.django_db
def test_my_tasks_with_rf(rf, django_user_model):
    user = django_user_model.objects.create_user(username='alice', password='x')
    Task.objects.create(title="Alice's task", owner=user)

    request = rf.get('/my-tasks/')
    request.user = user
    response = my_tasks(request)

    assert response.status_code == 200
    data = json.loads(response.content)
    assert len(data['results']) == 1