Checkpoint — Крок 6 завершено¶
Очікуваний результат запуску¶
crispy_notes_project (standalone)¶
$ python manage.py test hello_app.tests -v 1
Creating test database for alias 'default' ...
Found 129 test(s).
...........................................................................................................................ssssss
----------------------------------------------------------------------
Ran 129 tests in 90.2s
OK (skipped=6)
Destroying test database for alias 'default' ...
Ключ до виводу:
........... ← крапки = passed tests (123 passed)
s ← s = skipped (6 Selenium тестів без geckodriver)
F ← F = failed (assertion не спрацювала)
E ← E = error (виняток у тесті, не assertion)
OK (skipped=6) ← все зелено, 6 пропущено (очікувано)
Що означають цифри:
23 — test_models.py (unit: constraints, __str__, SET_NULL)
36 — test_services.py (unit: бізнес-логіка, security, транзакції)
23 — test_forms.py (unit: валідація, queryset security)
41 — test_views.py (integration: HTTP, ownership, redirects)
6 — test_selenium.py (E2E: браузер, skip без geckodriver)
───
129 — всього
notes_chat_app (Docker + PostgreSQL + WebSocket)¶
$ docker compose run --rm web python manage.py test \
notes_app.tests.test_models \
notes_app.tests.test_services \
notes_app.tests.test_forms \
notes_app.tests.test_views \
notes_app.tests.test_consumers \
-v 2
Creating test database for alias 'default' ...
System check identified no issues.
Found 129 test(s).
test_str_returns_hash_name ... ok
test_unique_constraint_same_user_same_name_raises ... ok
...
test_authenticated_member_can_connect ... ok
test_non_member_cannot_connect ... ok
...
----------------------------------------------------------------------
Ran 129 tests in 45.3s
OK
# Selenium (через exec, не run!)
$ docker compose exec web python manage.py test notes_app.tests.test_selenium -v 2
test_login_form_and_redirect ... ok
test_dashboard_shows_username ... ok
test_invalid_login_shows_error ... ok
----------------------------------------------------------------------
Ran 3 tests in 28.5s
OK
Чеклист по всіх рівнях тестування¶
Структура¶
- [ ]
tests/__init__.pyіснує (порожній файл — без нього Django не знайде тести) - [ ] Кожен тест-метод починається з
test_ - [ ]
setUp()дочірнього класу що перевизначає батьківський — викликаєsuper().setUp()
Unit тести (models, services, forms)¶
- [ ] Validators перевіряються через
full_clean(), а не.objects.create() - [ ] Після
delete()пов'язаного об'єкта →refresh_from_db()перед перевіркою - [ ] SET_NULL поведінка задокументована тестом
- [ ] Security тест: чужий тег/записник не приєднується
Integration тести (views)¶
- [ ] Integration тести використовують
force_login(), а неlogin() - [ ] Кожен захищений URL перевіряється на redirect для anonymous
- [ ] Bob отримує 404 для нотаток Alice (не 403)
- [ ] IDOR prevention: Alice не може POST у Bob's notebook
Consumer тести (notes_chat_app)¶
- [ ] Використовується
TransactionTestCase, а неTestCase - [ ]
communicator.scope['user']встановлений передconnect() - [ ]
await communicator.disconnect()у кожному тесті - [ ]
database_sync_to_asyncдля всіх звернень до БД з async коду
Selenium E2E¶
- [ ]
@unittest.skipUnless(SELENIUM_AVAILABLE, ...)на класі - [ ]
_DockerLiveServerMixinдля Docker-середовища (notes_chat_app) - [ ] Selenium:
execнеrunдля запуску в Docker - [ ]
manage.py testповертаєOK(навіть зskipped=6)
CI/CD¶
- [ ]
.github/workflows/ci.ymlу корені репозиторію - [ ] Job 1: unit + integration + consumer без selenium
- [ ] Job 2: selenium з
needs: test(запускається після Job 1) - [ ] PostgreSQL і Redis як GitHub Actions services
- [ ]
health-cmdдля PostgreSQL і Redis - [ ] Coverage report зберігається як artifact
Таблиця: що і де вчити¶
Unit тести¶
| Концепція | Де дивитись у коді |
|---|---|
| Базова структура тесту | Будь-який метод test_* — Arrange / Act / Assert |
| setUp і BaseTestCase | BaseServiceTest.setUp() в test_services.py |
| assertRaises | test_models.py — test_unique_constraint_same_user_same_name_raises |
| full_clean() vs save() | test_models.py — test_priority_above_4_raises_validation_error |
| refresh_from_db() | test_models.py — test_note_group_becomes_null_when_group_deleted |
| Persistence test | test_services.py — test_toggle_pin_note_persisted_in_db |
| Security (Mass Assignment) | test_services.py — test_create_note_ignores_other_users_tags |
| Транзакційна логіка | test_services.py — test_create_notebook_default_unsets_previous_default |
| Ізоляція між юзерами | test_services.py — test_create_notebook_default_does_not_affect_other_users |
| Form queryset security | test_forms.py — NoteFormSecurityTest |
Integration тести¶
| Концепція | Де дивитись у коді |
|---|---|
| force_login vs login | BaseViewTest.setUp() — self.client.force_login(alice) |
| @login_required check | AuthenticationViewTest — redirect для anonymous |
| response.status_code | NoteListViewTest.test_authenticated_gets_200 |
| assertContains / assertNotContains | NoteListViewTest.test_note_list_shows_only_user_notes |
| Ownership: 404 для чужого | NoteDetailViewTest.test_non_owner_gets_404 |
| Group-based access | NoteDetailViewTest.test_group_member_can_view_shared_note |
| IDOR через view | NoteCreateIDORTest.test_note_create_with_bobs_notebook_fails |
| Form errors у context | NoteCreateViewTest.test_invalid_post_returns_form_with_errors |
| assertRedirects | NoteDeleteViewTest.test_delete_redirects_to_note_list |
Consumer тести (notes_chat_app)¶
| Концепція | Де дивитись у коді |
|---|---|
| TransactionTestCase | GroupChatConsumerTest(TransactionTestCase) |
| WebsocketCommunicator | communicator.connect(), receive_json_from(), disconnect() |
| scope['user'] | communicator.scope['user'] = self.alice |
| database_sync_to_async | await database_sync_to_async(ChatMessage.objects.create)(...) |
| broadcast тест | test_send_message_saves_to_db_and_broadcasts |
E2E тести¶
| Концепція | Де дивитись у коді |
|---|---|
| StaticLiveServerTestCase | Базовий клас Selenium тестів |
| _DockerLiveServerMixin | host = '0.0.0.0', live_server_url → http://web:PORT |
| Session cookie trick | _login_via_cookie() — обходить login форму |
| Remote WebDriver | _make_driver() → SELENIUM_REMOTE_URL |
| implicitly_wait | cls.driver.implicitly_wait(5) — чекати до 5 сек на елемент |
| skipUnless | @unittest.skipUnless(SELENIUM_AVAILABLE, ...) |
| find_element + send_keys | test_login_form_and_redirect |
Практичне завдання¶
Завдання 1 — Написати тест (рівень: базовий)¶
Напиши тест що перевіряє: якщо юзер видаляє TodoList, всі пов'язані TodoItem видаляються разом (CASCADE).
# Підказка:
def test_todo_items_deleted_when_todo_list_deleted(self):
todo_list = TodoList.objects.create(user=self.alice, title='Shopping')
TodoItem.objects.create(todo_list=todo_list, text='Milk')
TodoItem.objects.create(todo_list=todo_list, text='Bread')
todo_list.delete()
self.assertEqual(TodoItem.objects.count(), ___) # що тут?
Завдання 2 — Security тест (рівень: середній)¶
Напиши тест що підтверджує: Bob (залогінений) не може відредагувати нотатку Alice (Bob отримує 404).
# Підказка:
def test_non_owner_cannot_edit_note(self):
alice_note = Note.objects.create(user=self.alice, title='Private')
self.client.force_login(self.bob)
response = self.client.get(
reverse('notes_app:note_edit', args=[alice_note.pk])
)
self.assertEqual(response.status_code, ___) # що тут?
Завдання 3 — Consumer тест (рівень: просунутий)¶
Напиши тест що підтверджує: якщо group з pk=999 не існує, consumer відхиляє з'єднання.
# Підказка: використовуй WebsocketCommunicator з неіснуючим pk
async def test_nonexistent_group_rejects_connection(self):
communicator = WebsocketCommunicator(
application,
'/ws/groups/999/chat/',
)
communicator.scope['user'] = self.alice
connected, code = await communicator.connect()
self.assertFalse(connected)
await communicator.disconnect()
Навігація¶
Попередній: Крок 5. Auth і Безпека — AuthN vs AuthZ, Sessions, IDOR, Group Sharing
Наступний: Крок 7. Async Django — ASGI, Django Channels, GroupChatConsumer, channel layer