27. (П13) Розробка та виконання тестів для REST API з використанням pytest¶
Передумови¶
- Прочитана Лекція 26 — Тестування REST API за допомогою pytest
- Виконана Практична 11 — Реалізація CRUD операцій і робота з базою даних у Flask
- Встановлений Docker
Персоналізація
У полі created_by при створенні книг повинно бути ваше ім'я та прізвище. На скріншотах у звіті повинно бути видно ваше ім'я у відповідях API та у виводі pytest.
Завдання¶
Написати автоматичні тести для Book Library API з Практичної 11. Замість ручного тестування через curl, ви створите набір pytest-тестів, які перевіряють коректність роботи API з PostgreSQL.
Підготовка середовища¶
PostgreSQL у Docker¶
docker run -d \
--name library-test-postgres \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
postgres:17
Проєкт¶
Підготовка застосунку¶
Візьміть app.py з Практичної 11 і перетворіть його на application factory (як у лекції 26):
- Винесіть конфігурацію БД у
DEFAULT_DB_CONFIG - Обгорніть усі маршрути у функцію
create_app(db_config=None) - Замініть глобальний
app = create_app()на блокif __name__ == "__main__":
Це дозволить тестам підставити окрему тестову базу даних.
Структура проєкту¶
library-api/
├── app.py # Flask-застосунок (application factory)
├── conftest.py # Фікстури pytest
├── test_authors.py # Тести для авторів
├── test_books.py # Тести для книг
└── requirements.txt
Фікстури (conftest.py)¶
Створіть файл conftest.py з фікстурами за зразком з лекції 26:
test_db(scope=session) — створює тестову базуlibrary_test_db, видаляє після всіх тестівapp(scope=session) — створює Flask-застосунок із тестовою БДclient(scope=function) — очищує таблиціbooksтаauthorsперед кожним тестом, повертає тестовий клієнт
Порядок очищення таблиць
При очищенні спочатку видаляйте books (бо має зовнішній ключ на authors), потім authors. Або використовуйте TRUNCATE books, authors RESTART IDENTITY CASCADE.
Частина 1: Тести для авторів¶
Створіть файл test_authors.py з класом TestAuthors. Напишіть тести для таких сценаріїв:
| Тест | Що перевіряє | Очікуваний статус |
|---|---|---|
test_get_authors_empty |
Список авторів порожній | 200, [] |
test_create_author |
Створення автора з name та birth_year |
201 |
test_create_author_without_name |
Створення без обов'язкового поля | 400 |
test_get_author_by_id |
Отримання автора за ID | 200 |
test_get_author_not_found |
Запит неіснуючого автора | 404 |
test_delete_author |
Видалення автора | 204 |
test_delete_author_not_found |
Видалення неіснуючого автора | 404 |
Вимоги до тестів:
- При створенні автора перевіряйте, що у відповіді є поля
id,name,birth_year - Після видалення автора перевіряйте через GET, що він більше недоступний (404)
Приклад тесту:
class TestAuthors:
def test_create_author(self, client):
response = client.post("/api/authors", json={
"name": "Тарас Шевченко",
"birth_year": 1814,
})
assert response.status_code == 201
data = response.get_json()
assert data["name"] == "Тарас Шевченко"
assert data["birth_year"] == 1814
assert "id" in data
Частина 2: Тести для книг¶
Створіть файл test_books.py з класом TestBooks. Напишіть тести для таких сценаріїв:
| Тест | Що перевіряє | Очікуваний статус |
|---|---|---|
test_get_books_empty |
Список книг порожній | 200, [] |
test_create_book |
Створення книги з усіма полями | 201 |
test_create_book_without_title |
Створення без title |
400 |
test_create_book_without_created_by |
Створення без created_by |
400 |
test_create_book_with_author |
Створення книги з прив'язкою до автора | 201 |
test_create_book_with_nonexistent_author |
Прив'язка до неіснуючого автора | 400 |
test_get_book_by_id |
Отримання книги за ID | 200 |
test_get_book_not_found |
Запит неіснуючої книги | 404 |
test_delete_book |
Видалення книги | 204 |
Вимоги до тестів:
- У полі
created_byвикористовуйте ваше ім'я та прізвище - При створенні книги з автором спочатку створіть автора через POST, потім використайте його
id - Після видалення книги перевіряйте через GET, що вона більше недоступна
Приклад тесту:
class TestBooks:
def test_create_book_with_author(self, client):
# Спочатку створюємо автора
author = client.post("/api/authors", json={
"name": "Леся Українка",
"birth_year": 1871,
}).get_json()
# Створюємо книгу з прив'язкою до автора
response = client.post("/api/books", json={
"title": "Lisova Pisnia",
"genre": "drama",
"year_published": 1911,
"author_id": author["id"],
"created_by": "Ім'я Прізвище",
})
assert response.status_code == 201
data = response.get_json()
assert data["author_id"] == author["id"]
assert data["created_by"] == "Ім'я Прізвище"
Частина 3: Тести для фільтрації та зв'язків¶
Додайте до test_books.py клас TestBooksFilter та до test_authors.py клас TestAuthorBooks:
Фільтрація книг (TestBooksFilter):
| Тест | Що перевіряє |
|---|---|
test_filter_by_genre |
GET /api/books?genre=poetry повертає лише книги з жанром poetry |
test_filter_by_author_id |
GET /api/books?author_id=1 повертає лише книги автора |
test_search_by_title |
GET /api/books?q=kobzar знаходить книгу за частковим збігом |
test_filter_no_results |
Фільтрація за неіснуючим жанром повертає [] |
Книги автора (TestAuthorBooks):
| Тест | Що перевіряє |
|---|---|
test_get_author_books |
GET /api/authors/<id>/books повертає книги автора |
test_get_author_books_empty |
Автор без книг повертає [] |
test_get_author_books_not_found |
Неіснуючий автор повертає 404 |
Приклад тесту з підготовкою даних:
class TestBooksFilter:
def test_filter_by_genre(self, client):
# Підготовка: створюємо книги різних жанрів
client.post("/api/books", json={
"title": "Kobzar",
"genre": "poetry",
"created_by": "Ім'я Прізвище",
})
client.post("/api/books", json={
"title": "Tygrolovy",
"genre": "novel",
"created_by": "Ім'я Прізвище",
})
# Фільтрація
response = client.get("/api/books?genre=poetry")
data = response.get_json()
assert len(data) == 1
assert data[0]["title"] == "Kobzar"
Частина 4: Тест видалення автора з книгами¶
Додайте до test_authors.py тест, що перевіряє каскадну поведінку ON DELETE SET NULL:
| Тест | Що перевіряє |
|---|---|
test_delete_author_keeps_books |
Після видалення автора його книги залишаються, але author_id стає null |
def test_delete_author_keeps_books(self, client):
# Створюємо автора та книгу
author = client.post("/api/authors", json={
"name": "Іван Франко",
}).get_json()
book = client.post("/api/books", json={
"title": "Zakhar Berkut",
"author_id": author["id"],
"created_by": "Ім'я Прізвище",
}).get_json()
# Видаляємо автора
client.delete(f"/api/authors/{author['id']}")
# Книга залишилась, але без автора
response = client.get(f"/api/books/{book['id']}")
assert response.status_code == 200
assert response.get_json()["author_id"] is None
Запуск тестів¶
Очікуваний результат:
test_authors.py::TestAuthors::test_get_authors_empty PASSED
test_authors.py::TestAuthors::test_create_author PASSED
test_authors.py::TestAuthors::test_create_author_without_name PASSED
test_authors.py::TestAuthors::test_get_author_by_id PASSED
test_authors.py::TestAuthors::test_get_author_not_found PASSED
test_authors.py::TestAuthors::test_delete_author PASSED
test_authors.py::TestAuthors::test_delete_author_not_found PASSED
test_authors.py::TestAuthorBooks::test_get_author_books PASSED
test_authors.py::TestAuthorBooks::test_get_author_books_empty PASSED
test_authors.py::TestAuthorBooks::test_get_author_books_not_found PASSED
test_authors.py::TestAuthors::test_delete_author_keeps_books PASSED
test_books.py::TestBooks::test_get_books_empty PASSED
test_books.py::TestBooks::test_create_book PASSED
test_books.py::TestBooks::test_create_book_without_title PASSED
test_books.py::TestBooks::test_create_book_without_created_by PASSED
test_books.py::TestBooks::test_create_book_with_author PASSED
test_books.py::TestBooks::test_create_book_with_nonexistent_author PASSED
test_books.py::TestBooks::test_get_book_by_id PASSED
test_books.py::TestBooks::test_get_book_not_found PASSED
test_books.py::TestBooks::test_delete_book PASSED
test_books.py::TestBooksFilter::test_filter_by_genre PASSED
test_books.py::TestBooksFilter::test_filter_by_author_id PASSED
test_books.py::TestBooksFilter::test_search_by_title PASSED
test_books.py::TestBooksFilter::test_filter_no_results PASSED
========================= 24 passed =========================
Результат¶
Після виконання роботи студент здає:
app.py— Flask-застосунок з application factoryconftest.py— фікстури для тестування з PostgreSQLtest_authors.py— тести для авторів (класиTestAuthors,TestAuthorBooks)test_books.py— тести для книг (класиTestBooks,TestBooksFilter)- Звіт зі скріншотом виконання
pytest -v, де всі тести проходять та видно ваше ім'я у назвах тестових даних
Знайшли помилку чи бажаєте додати інформацію, щоб покращити курс? Створіть issue на GitHub