Сегодня мы возьмем всё, что узнали о тестировании API, и применим эти знания на практике. В центре нашего внимания окажутся тесты для существующих эндпоинтов нашего проекта. Мы будем писать тесты для CRUD-операций, проверять авторизацию и разрешения, а также отлавливать потенциальные баги.
Создаём тесты для CRUD-операций
Наша цель — протестировать базовые операции CRUD для начального API, который мы разрабатывали ранее. До этого момента, у нас уже есть модель Book с полями title и author, а также эндпоинты для этого ресурса.
Вспомнить: эндпоинты для модели Book
GET /api/books/— получить список всех книг;POST /api/books/— создать новую книгу;GET /api/books/<id>/</id>— получить информацию о книге по её ID;PUT /api/books/<id>/</id>— обновить информацию о книге;DELETE /api/books/<id>/</id>— удалить книгу.
Начальная подготовка Создайте файл test_views.py в директории с тестами вашего приложения (например, books/tests/). После этого можно приступать.
Тесты для списка книг (GET)
Сначала мы проверим, что наш эндпоинт /api/books/ возвращает корректный результат.
import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from books.models import Book
@pytest.mark.django_db
def test_get_books_list():
# Создаём тестовые данные
Book.objects.create(title="Django for Beginners", author="William S. Vincent")
Book.objects.create(title="Python Crash Course", author="Eric Matthes")
# Создаем клиент для тестов
client = APIClient()
# Выполняем GET-запрос
response = client.get(reverse('books-list'))
# Проверяем статус ответа
assert response.status_code == status.HTTP_200_OK
# Убеждаемся, что возвращённые данные корректны
data = response.json()
assert len(data) == 2
assert data[0]['title'] == "Django for Beginners"
assert data[1]['title'] == "Python Crash Course"
Здесь мы проверяем, что:
- Наш эндпоинт возвращает успешный ответ с кодом
200; - Содержимое ответа соответствует нашим тестовым данным.
Тесты для создания книги (POST)
Теперь давайте протестируем, что эндпоинт для создания книги работает как положено.
@pytest.mark.django_db
def test_create_book():
# Создаем клиент для тестов
client = APIClient()
# Создаем запрос на добавление книги
payload = {
"title": "Two Scoops of Django",
"author": "Audrey Roy Greenfeld"
}
response = client.post(reverse('books-list'), payload, format='json')
# Проверяем статус ответа
assert response.status_code == status.HTTP_201_CREATED
# Убеждаемся, что книга создалась в базе данных
book = Book.objects.get(id=response.data['id'])
assert book.title == payload['title']
assert book.author == payload['author']
Чистый, простой тест для добавления книги. Мы:
- Проверяем, что статус ответа —
201 CREATED; - Проверяем, что объект действительно сохранился в базе данных с указанными полями.
Тесты для получения книги (GET /<id>/)</id>
Следующий шаг — проверить, что мы можем получить конкретный объект по его ID.
@pytest.mark.django_db
def test_get_book_detail():
# Создаём тестовые данные
book = Book.objects.create(title="Effective Python", author="Brett Slatkin")
# Создаем клиент для тестов
client = APIClient()
# Выполняем GET-запрос
response = client.get(reverse('books-detail', kwargs={'pk': book.id}))
# Проверяем статус ответа
assert response.status_code == status.HTTP_200_OK
# Убеждаемся, что данные ответа совпадают с данными книги
data = response.json()
assert data['title'] == book.title
assert data['author'] == book.author
Максимально подробно проверяем, что возвращаются именно те данные, которые связаны с конкретным ID.
Тесты для обновления книги (PUT)
Теперь проверим, как работает обновление книги.
@pytest.mark.django_db
def test_update_book():
# Создаём тестовые данные
book = Book.objects.create(title="Python Tricks", author="Dan Bader")
# Создаем клиент для тестов
client = APIClient()
# Данные для обновления
payload = {
"title": "Python Tricks Updated",
"author": "Dan Bader"
}
response = client.put(reverse('books-detail', kwargs={'pk': book.id}), payload, format='json')
# Проверяем статус ответа
assert response.status_code == status.HTTP_200_OK
# Проверяем, что книга обновилась
book.refresh_from_db()
assert book.title == payload['title']
assert book.author == payload['author']
Мы здесь используем метод refresh_from_db, чтобы обновить объект из базы данных.
Тесты для удаления книги (DELETE)
Наконец, протестируем удаление объекта.
@pytest.mark.django_db
def test_delete_book():
# Создаём тестовые данные
book = Book.objects.create(title="Learn Python the Hard Way", author="Zed Shaw")
# Создаем клиент для тестов
client = APIClient()
# Удаляем книгу
response = client.delete(reverse('books-detail', kwargs={'pk': book.id}))
# Проверяем статус ответа
assert response.status_code == status.HTTP_204_NO_CONTENT
# Убеждаемся, что книга удалена из базы данных
assert not Book.objects.filter(id=book.id).exists()
Книга удаляется успешно, а мы проверяем, что она больше не существует в базе данных.
Анализ и исправление ошибок в тестах
Ошибки во время тестирования — это нормально. Если вы столкнулись с багом или тест не проходит, надо действовать последовательно:
- Проверьте сообщение об ошибке. Оно часто подскажет, где именно есть проблема.
- Используйте print или breakpoints для отладки теста.
- Запускайте тесты изолированно, чтобы определить, какой именно из них вызывает проблему (
pytest -k test_name).
Предположим, что вы забыли зарегистрировать ваш эндпоинт в urls.py. Тесты сразу покажут ошибку NoReverseMatch, что поможет вам моментально исправить проблему.
Практика: написание тестов
Попробуйте написать тесты для следующих случаев:
- Проверка, что при отправке недопустимых данных (
titleилиauthorпустые) наш API возвращает ошибку валидации с кодом400 BAD REQUEST. - Тест для проверки, что пагинированный эндпоинт возвращает заданное количество объектов на странице.
- Проверьте, что у неавторизованного пользователя нет доступа к созданию книги (если у вас включён такой сценарий).
На этом всё! Вы написали тесты для CRUD-операций и научились анализировать результаты тестирования. Благодаря такому подходу вы теперь уверены, что ваши API эндпоинты работают корректно, а изменения в коде не ломают существующий функционал.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ