#40. Тестування у Django, DRF
Тестування у Django, DRF
План
- Тестування в Django
- Особливості тестування
- Client
- Тестування в DRF
- APITestCase
- APIClient
- Фабричний метод і фабрики
- RequestFactory
- Factory Boy, Fuzzy attributes
- Faker
- Література
Види тестування
Тестувальники діляться на Manual і Automation.
Мануальні займаються тим, що вручну перевіряють весь доступний функціонал. Автоматизатори пишуть код для тестування продукту.
Рівні тестування
Існує 4 основні рівні тестування функціоналу.
Модульні тести (Unit Tests) - це тести, що перевіряють функціонал конкретного модуля мінімального розміру.
Якщо ви переписали метод get_context_data(), то юніт тестом буде спроба викликати цей метод з різними вхідними даними, і подивитися на те, що поверне результат.
Інтеграційні тести (Integration Tests) - це вид тестування, коли перевіряється цілісність роботи системи, без сторонніх засобів. Наприклад, ви переписали метод get_context_data(), виконуємо запит за допомогою коду і дивимося,
чи змінилася змінна context у відповіді на наш запит.
Приймальні тести (Acceptance Tests) - вид тестів із повною імітацією дій користувача. За допомогою спеціальних засобів (наприклад, Selenium) ми прописуємо код відкриття браузера, пошуку необхідних елементів на сторінці, імітуємо введення даних, натискання кнопок, перехід за посиланнями тощо.
Ручні тести (Manual Tests) - вид тестів, коли ми повністю повторюємо потенційні дії користувача.
Як це взагалі працює?
Теоретично можна написати робочий проєкт взагалі без жодного тесту (ваш модуль тому приклад). Але чим більше і складніше система, тим дорожча вартість помилки або обсяг витраченого часу на пошук причини цієї помилки.
У реальності навіть не дуже великий проєкт не може існувати без тестування.
Хто пише юніт тести?
Юніт тести - це тести конкретно написаної функції або методу. А значить, що знання про те, як це працює, є
тільки у розробника, а отже, їх пише розробник.
Ідеальний світ - розробник покриває все тестами.
Реальність - розробник покриває основний функціонал і тонкі місця тестами.
Найгірший випадок - юніт тестів немає, що призводить до ускладнення написання і модифікації проєкту в кілька разів.
Хто пише інтеграційні тести?
Ідеальний світ - Automation QA, причому цілком можливо, що іншою мовою програмування, застосунку неважливо, хто надсилає HTTP-запит, з якої платформи та мови.
Реальність - Automation QA, якщо вони є, розробники, якщо QA немає.
Найгірший випадок - немає інтеграційних тестів. Що призводить до того, що при впровадженні нових фіч можна не дізнатися про те, що зламалася стара. Це призводить до того, що функціонал відвалюватиметься швидше, ніж розроблятиметься.
Хто пише приймальні тести?
Ідеальний світ - все ті ж автоматизатори.
Реальність - у кого є час і бажання, найчастіше цей вид тестування або ігнорується, або виконується, коли уже все інше написано. Також буває, коли через такий вид тестування мануальників навчають і залучають до автоматизації.
Найгірший випадок - приймальних тестів немає, і в разі відсутності мануальної перевірки можна не дізнатися, що функціонал в браузері більше не працює.
Хто виконує ручні тести?
Ідеальний світ - Manual QA.
Реальність - якщо є Manual QA, то вони, якщо ні, то Automation QA, якщо і їх немає, то розробники в процесі розробки.
Найгірший випадок - не проводяться, впевненість, що функціонал працює, дорівнює нулю.
Дослідницьке тестування та планування
Хороша новина в тому, що ви, ймовірно, вже створювали тести, не усвідомлюючи цього. Пам’ятаєте, коли ви запускали додаток і використовували його вперше? Ви перевіряли функції та експериментували з ними? Це називається дослідницьке тестування і є формою ручного тестування.
Дослідницьке тестування - це форма тестування, яка проводиться без плану. У такому виді тестування ви просто вивчаєте додаток.
Щоб отримати повний набір ручних тестів, потрібно скласти список усіх функцій вашого застосунку, список різних типів вхідних даних, які приймає ваш додаток, і всі очікувані результати. Тепер щоразу, коли ви будете вносити зміни до свого коду, вам потрібно переглянути кожен елемент у цьому списку і перевірити його правильність.
Це не виглядає весело?
Ось де приходить на допомогу тест план.
Тест-план - це поділ вашого додатка на мінімальні частини та опис очікуваної роботи функціоналу кожної частини, порядку їх виконання та очікувані результати.
Якщо є тест-план, ви можете щоразу проходити за всіма його пунктами і бути впевненим, що ви перевірили все. У разі оновлення застосунку необхідно оновити і план.
Test Case
Документ, що описує сукупність кроків, конкретних умов та параметрів, необхідних для перевірки реалізації функції, що тестується чи її частини.

Test Suite
Це набір тестів, які призначені для тестування програми, щоб показати, що вона має певний набір поведінки.
Тестування в Django
Якщо ви пишете тести для веб-додатків, то варто пам’ятати про важливі відмінності в написанні та запуску таких тестів.
Подумайте про код, який потрібно протестувати у веб-додатку. Усі маршрути, уявлення і моделі вимагають багато імпорту і знань про використовуваний фреймворк.
Це схоже на тестування автомобіля: перед тим, як провести прості тести, на кшталт перевірки роботи фар, потрібно увімкнути комп’ютер у машині.
Тестування сайту - це складне завдання, тому що воно складається з декількох логічних шарів - від HTTP-запиту і запиту до моделей, до валідації форми та їх обробки, а крім того, рендерингу шаблонів сторінок.
Django надає фреймворк для створення тестів, побудованого на основі ієрархії класів, які, своєю чергою, залежать від стандартної бібліотеки Python unittest. Незважаючи на назву, цей фреймворк підходить і для юніт-, і для інтеграційного тестування. Фреймворк Django додає методи та інструменти, які допомагають тестувати як веб так і, специфічну для Django, поведінку. Це дозволяє вам імітувати URL-запити, додавання тестових даних, а також проводити перевірку вихідних даних ваших додатків.
from django.test import TestCase
class MyTestCase(TestCase):
def setUp(self):
# Запускається перед кожним тестом
pass
def tearDown(self):
# Запускається після кожного тесту
pass
def test_something_that_will_pass(self):
self.assertFalse(False)
def test_something_that_will_fail(self):
self.assertTrue(False)
Основна відмінність від минулих прикладів - потрібно успадковувати з django.test.TestCase, а не unittest.TestCase.
TestCase.mro()
(<class 'django.test.testcases.TestCase'>,
<class 'django.test.testcases.TransactionTestCase'>,
<class 'django.test.testcases.SimpleTestCase'>,
<class 'unittest.case.TestCase'>,
<class 'object'>)
SimpleTestCase
SimpleTestCase успадковується від базового.
Що додає?
Додає settings.py в структуру тесту і можливість переписати або змінити settings.py для тесту.
Додає Client, який використовується для написання інтеграційних тестів (через нього ми будемо відправляти запити).
Додає нові методи assert:
assertContains(response, text)
assertNotContains(response, text)
assertTemplateUsed(response, template_name)
assertTemplateNotUsed(response, template_name)
assertURLEqual(url1, url2)
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
assertHTMLEqual(html1, html2)
assertHTMLNotEqual(html1, html2)
assertXMLEqual(xml1, xml2)
assertXMLNotEqual(xml1, xml2)
assertJSONEqual(raw, expected_data)
assertJSONNotEqual(raw, expected_data)
TransactionTestCase
TransactionTestCase успадковується від SimpleTestCase.
Що додає?
Додає можливість виконувати транзакції в базу даних у рамках тесту.
Додає атрибут fixtures для можливості завантажувати базові умови тесту з фікстур.
Додає атрибут reset_sequences, який дозволяє скидати послідовності для кожного тесту (кожен створений
об’єкт завжди починатиметься з id=1)
Додає нові методи assert:
assertQuerysetEqual - перевірка на те, що отриманий кверисет збігається з очікуваним.
assertNumQueries - перевірка на те, що виконання функції робить певну кількість запитів до бази.
TestCase з модуля Django
TestCase успадковується від TransactionTestCase.
Що додає?
По суті нічого. :) Трохи по-іншому виконує запити в базу (з використанням атомарності), через що
краще.
Додатковий метод setUpTestData() для опису даних для тесту. Не обов’язковий.
Це найбільш часто використовуваний вид тестів.
LiveServerTestCase
LiveServerTestCase успадковується від TransactionTestCase.
Що додає?
Запускає реальний сервер для можливості відкрити проєкт у браузері. Необхідний для написання Acceptance Tests.
Найчастіше в таких тестах запускається сервер та імітація браузера (наприклад, Selenium).
Решта в документації
Налаштування для тестування
База даних для тестування
Для тестів використовується окрема база даних, яка буде вказана у змінній TEST у змінній DATABASES у файлі ``settings.py`:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'USER': 'mydatabaseuser',
'NAME': 'mydatabase',
'TEST': {
'NAME': 'mytestdatabase',
},
},
}
Ця база буде спочатку порожня, і буде очищатися після кожного виконаного тест кейса. Щоб не очищати базу, можна вказати параметр --keepdb (або -k)
Налаштування в PyCharm

Структура тестів
Незважаючи на те, що Django створює для нас у додатку файл tests.py, ним практично ніколи не користуються.
Існує два найпоширеніших способи зберігання тестів. Якщо вам пощастило і на вашому проєкті є спеціальні
тестувальники, то ваше завдання - це тільки юніт тести.
І тоді в папці додатка створюється ще одна папка tests, в якій вже створюються файли для тестів різних частин, наприклад, test_models.py, test_forms.py тощо.
Якщо вам не пощастило, і на проекті ви за автоматичних тестерів, то тоді в цій же папці (tests) створюється ще 3 папки unit, integration і acceptance, і вже в них описуються різні тести.
Запуск тестів
Припустимо, що в нас у додатку animals є папка tests, у ній папка unit і в ній файл test_models.
from django.test import TestCase
from myapp.models import Animal
class AnimalTestCase(TestCase):
def setUp(self):
Animal.objects.create(name="lion", sound="roar")
Animal.objects.create(name="cat", sound="meow")
def test_animals_can_speak(self):
""""Тварини, які вміють говорити, правильно ідентифікуються""""
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
self.assertEqual(lion.speak(), 'Лев каже "реви"')
self.assertEqual(cat.speak(), 'Кіт каже "мяу"')
Для виконання тестового набору використовуйте manage.py test замість unittest у командному рядку:
python manage.py test
Якщо вам потрібно кілька тестових файлів, замініть tests.py на папку tests, покладіть у неї порожній файл із назвою __init__.py і створіть файли test_*.py. Django виявить їх і виконає.
# Запустити всі тести в додатку в папці тестів
$./manage.py test animals.tests
# Запустити всі тести в додатку
$./manage.py test animals
# Запустити один тест кейс
$./manage.py test animals.tests.unit.test_models.AnimalTestCase
# Запустити один тест із тест кейса
$./manage.py test animals.tests.unit.test_models.AnimalTestCase.test_animals_can_speak
Спеціальні інструменти тестування
Сlient
Для проведення інтеграційного тестування джанго застосунку нам необхідно надсилати запити з клієнта (браузера), функціонал для цього нам надано з коробки, і ми можемо ним скористатися:
>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'
Такий запит не вимагатиме CSRF токену
Підтримує метод login() і force_login()
c = Client()
c.login(username='fred', password='secret')
після чого запити надходитимуть від авторизованого користувача і метод force_login, який приймає об’єкт юзера, а не логін і пароль
client.force_login(user)
і метод logout()
client.logout()
GET
>>> c = Client()
>>> response = c.get('/customers/details/', {'name': 'fred', 'age': 7})
надішле GET запит на
/customers/details/?name=fred&age=7
І response запише відповідь
POST
>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})
надішле POST запит на
/login/
response
response = {TemplateResponse} <TemplateResponse status_code=200, "text/html; charset=utf-8">
charset = {str} 'utf-8'
client = {Client} <django.test.client.Client object at 0x7f1b7b1ed850>
closed = {bool} True
content = {bytes: 2495} b'<!DOCTYPE html>\n<html lang="en">\n<head>\n \n <title>Local Library</title>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/d
context = {ContextList: 2} [[{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0x7f1b7a78d790>>, 'request': <WSGIRequest: GET '/catalog/authors/'>, 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser
context_data = {dict: 6} {'paginator': <django.core.paginator.Paginator object at 0x7f1b7a771dc0>, 'page_obj': <Page 1 of 2>, 'is_paginated': True, 'object_list': <QuerySet [<Author: Surname 0, Christian 0>, <Author: Surname 1, Christian 1>, <Author: Surname 10, Christian 10>, <Au
cookies = {SimpleCookie: 0}
exc_info = {NoneType} None
is_rendered = {bool} True
json = {partial} functools.partial(<bound method ClientMixin._parse_json of <django.test.client.Client object at 0x7f1b7b1ed850>>, <TemplateResponse status_code=200, "text/html; charset=utf-8">)
reason_phrase = {str} 'OK'
rendered_content = {SafeString} <!DOCTYPE html>\n<html lang="en">\n<head>\n \n <title>Local Library</title>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">\n\n \n <!-- Add additional CSS in static file -->\n \n <link rel="stylesheet" href="/static/css/styles.78e88d8d7ee5.css">\n</head>\n<body>\n\n<div class="container-fluid">\n\n<div class="row">\n <div class="col-sm-2">\n \n <ul class="sidebar-nav">\n <li><a href="/catalog/">Home</a></li>\n <li><a href="/catalog/books/">All books</a></li>\n <li><a href="/catalog/authors/">All authors</a></li>\n </ul>\n \n <ul class="sidebar-nav">\n \n <li><a href="/accounts/login/?next=/catalog/authors/">Login</a></li> \n \n </ul>\n \n \n \n\n </div>\n <div class="col-sm-10 ">\n \n\n<h1>Author List</h1>\n\n\n <ul>\n\n \n <li>\n...
rendering_attrs = {list: 4} ['template_name', 'context_data', '_post_render_callbacks', '_request']
request = {dict: 5} {'PATH_INFO': '/catalog/authors/', 'REQUEST_METHOD': 'GET', 'SERVER_PORT': '80', 'wsgi.url_scheme': 'http', 'QUERY_STRING': ''}
resolver_match = {ResolverMatch} ResolverMatch(func=catalog.views.AuthorListView, args=(), kwargs={}, url_name=authors, app_names=[], namespaces=[], route=catalog/authors/)
status_code = {int} 200
streaming = {bool} False
template_name = {list: 1} ['catalog/author_list.html']
templates = {list: 2} [<django.template.base.Template object at 0x7f1b7a6dbcd0>, <django.template.base.Template object at 0x7f1b7a7a6ee0>]
using = {NoneType} None
wsgi_request = {WSGIRequest} <WSGIRequest: GET '/catalog/authors/'>
Клієнт одразу є в тест кейсі, його немає потреби створювати, до нього можна звернутися через self.client.
class SimpleTest(TestCase):
def test_details(self):
response = self.client.get('/customer/details/')
self.assertEqual(response.status_code, 200)
def test_index(self):
response = self.client.get('/customer/index/')
self.assertEqual(response.status_code, 200)
Завантаження фікстур
Усе дуже просто, якщо у вас лежить файл із фікстурами, то достатньо його просто вказати в атрибутах.
from myapp.models import Animal
class AnimalTestCase(TestCase):
fixtures = ['mammals.json', 'birds']
def setUp(self):
# Test definitions as before.
call_setup_methods()
def test_fluffy_animals(self):
# A test that uses the fixtures.
call_some_test_code()
Завантажиться файл mammals.json, і з нього фікстура birds.
Теги для тестів
Існує можливість поставити “тег” на кожен тест, а після запускати тільки ті, що з певним тегом.
class SampleTestCase(TestCase):
@tag('fast')
def test_fast(self):
...
@tag('slow')
def test_slow(self):
...
@tag('slow', 'core')
def test_slow_but_core(self):
...
Або навіть цілий тесткейс:
@tag('slow', 'core')
class SampleTestCase(TestCase):
...
Після чого запускати із зазначенням тега.
./manage.py test --tag=fast
Тестування management-команд
Для цього використовується спеціальний метод call_command():
from io import StringIO
from django.core.management import call_command
from django.test import TestCase
class ClosePollTest(TestCase):
def test_command_output(self):
out = StringIO()
call_command('closepoll', stdout=out)
self.assertIn('Expected output', out.getvalue())
Пропуск тестів
Тести можна пропускати залежно від умов і деталей запуску.
Дока тут
Тестування View
models.py
class AuthorListView(generic.ListView):
model = Author
paginate_by = 10
test_views.py
from django.test import TestCase
from django.contrib.auth.models import User
from catalog.models import Author
from django.urls import reverse
from http import HTTPStatus
class AuthorListViewTest(TestCase):
def setUp(self):
# Create a user
self.user = User.objects.create_user(username='user_1', password='1X<ISRUkw+tuK')
self.client.force_login(user=self.user) # APIClient()
self.author = Author.objects.create(first_name='Christian', last_name='Author')
def test_index(self):
response = self.client.get(reverse('authors'))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'catalog/author_list.html')
self.assertContains(response, 'Christian')
def test_view(self):
response = self.client.get(reverse('authors'), args=[self.author.id])
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Christian')
self.assertTemplateUsed(response, 'catalog/author_view.html')
def test_create(self):
url = reverse('author-create')
data = {
'first_name': 'First Name',
'last_name': 'Surname',
}
response = self.client.post(url, data)
# Manually check redirect because we don't know what author was created
self.assertEqual(response.status_code, 302)
self.assertTrue(response.url.startswith('/catalog/author/')
Як значення кодів статусу можна використовувати HTTPStatus
>>> from http import HTTPStatus
>>> HTTPStatus.OK
<HTTPStatus.OK: 200>
self.assertEqual(resp.status_code, HTTPStatus.OK) # 200
self.assertEqual(resp.status_code, HTTPStatus.FOUND) # 302
self.assertEqual(resp.status_code, HTTPStatus.FORBIDDEN) # 403
self.assertEqual(resp.status_code, HTTPStatus.NOT_FOUND) # 404
Тестування REST API
У DRF для тестування є APITestCase, який дублює TestCase з Django, але в якості клієнта виступає APIClient
from rest_framework.test import APITestCase
APITestCase.mro()
[<class 'rest_framework.test.APITestCase'>,
<class 'django.test.testcases.TestCase'>,
<class 'django.test.testcases.TransactionTestCase'>,
<class 'django.test.testcases.SimpleTestCase'>,
<class 'unittest.case.TestCase'>,
<class 'object'>]
from django.test import testcases
class APITestCase(testcases.TestCase):
client_class = APIClient
APIClient
У DRF є свій клієнт для запитів, у якому вже прописано всі необхідні методи запитів (get(), post(), тощо).
from rest_framework.test import APIClient
client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')
Авторизація через клієнт
# Make all requests in the context of a logged in session.
client = APIClient()
client.login(username='lauren', password='secret')
# Log out
client.logout()
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient
# Include an appropriate `Authorization:` header on all requests.
token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
Так само підтримує force_authenticate
user = User.objects.get(username='lauren')
client = APIClient()
client.force_authenticate(user=user)
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from myproject.apps.core.models import Account
class AccountTests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='user_1', password='1X<ISRUkw+tuK')
#self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_view(self):
#краще використовувати response.data, а не json.loads(response.content)
data = {'id': 4, 'username': 'lauren'}
response = self.client.get('/users/4/')
self.assertEqual(response.data, data)
def test_create_account(self):
"""
Ensure we can create a new account object.
"""
url = reverse('account-list')
data = {'name': 'DabApps'}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Account.objects.count(), 1)
self.assertEqual(Account.objects.get().name, 'DabApps')
Фабричний метод і фабрики
Фабричний метод (Factory Method) - породжувальний патерн проєктування, який визначає інтерфейс для створення об’єктів деякого класу, але безпосереднє рішення про те, об’єкт якого класу створювати, відбувається в підкласах. Тобто патерн передбачає, що базовий клас делегує створення об’єктів класам-спадкоємцям.
RequestFactory (Фабрика запитів)
Клас RequestFactory використовує той самий API, що й тестовий клієнт. Однак замість того, щоб поводитися як браузер, RequestFactory пропонує спосіб створення екземпляра запиту, який можна використовувати як перший параметр будь-якого подання. Це дає змогу тестувати функцію перегляду так само, як ви тестуєте будь-яку іншу функцію, наприклад чорну скриньку, з відомими даними в якості вхідних даних і тестуванням певного результату.
API RequestFactory трохи більш обмежений, ніж у тестового клієнта:
Це дає доступ до методів HTTP тільки get() , post() , put() , delete() , head() , options() і trace().
Усі ці методи приймають одні й ті самі параметри. Оскільки це тільки фабрика, яка виробляє запити, ви несете відповідальність за відповідь.
Він не підтримує проміжне ПЗ. Атрибути сеансу й автентифікації мають бути надані самим тестом, якщо необхідно, для правильної роботи подання.
Для чого необхідний RequestFactory? Не для всіх перевірок потрібно робити запит, часто необхідно його тільки імітувати.
from django.test import RequestFactory, TestCase
from .views import MyView, my_view
class SimpleTest(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = User.objects.create_user(
username='jacob', email='jacob@…', password='top_secret')
def test_details(self):
# Create an instance of a GET request.
request = self.factory.get('/customer/details')
# Recall that middleware are not supported. You can simulate a
# logged-in user by setting request.user manually.
request.user = self.user
# Or you can simulate an anonymous user by setting request.user to
# an AnonymousUser instance.
request.user = AnonymousUser()
# Test my_view() as if it were deployed at /customer/details
response = my_view(request)
# Use this syntax for class-based views.
response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200)
Тестування окремих методів CBV
Для тестування окремих методів ми можемо використовувати метод setup().
Наприклад, якщо ми замінили get_context_data(), то можна зробити так:
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = 'myapp/home.html'
def get_context_data(self, **kwargs):
kwargs['environment'] = 'Production'
return super().get_context_data(**kwargs)
from django.test import RequestFactory, TestCase
from .views import HomeView
class HomePageTest(TestCase):
def test_environment_set_in_context(self):
request = RequestFactory().get('/')
view = HomeView()
view.setup(request)
context = view.get_context_data()
self.assertIn('environment', context)
Factory Boy
pip install factory_boy
Прописування в setUp створення нових об’єктів може займати дуже багато часу. Щоб це прискорити, спростити й автоматизувати, можна написати свою фабрику
DjangoModelFactory
Усі фабрики для моделі Django повинні використовувати базовий клас DjangoModelFactory.
from factory.django import DjangoModelFactory
from . import base
class UserFactory(DjangoModelFactory):
class Meta:
model = base.User
firstname = "John"
lastname = "Doe"
>>>john = UserFactory()
<User: John Doe>
>>>jack = UserFactory(firstname="Jack")
<User: Jack Doe>
На один клас можна створювати кілька фабрик
class EnglishUserFactory(DjangoModelFactory):
class Meta:
model = base.User
firstname = "John"
lastname = "Doe"
lang = 'en'
class FrenchUserFactory(DjangoModelFactory):
class Meta:
model = base.User
firstname = "Jean"
lastname = "Dupont"
lang = 'fr'
EnglishUserFactory()
<User: John Doe (en)>
>>> FrenchUserFactory()
<User: Jean Dupont (fr)>
Sequences
class UserFactory(DjangoModelFactory):
class Meta:
model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
LazyFunction
При створенні можна використовувати функції, які не залежать то класу
class LogFactory(DjangoModelFactory):
class Meta:
model = models.Log
timestamp = factory.LazyFunction(datetime.now)
>>> LogFactory()
<Log: log at 2016-02-12 17:02:34>
>>> # The LazyFunction can be overridden
>>> LogFactory(timestamp=now - timedelta(days=1))
<Log: log at 2016-02-11 17:02:34>
LazyAttribute
Деякі поля можуть бути заповнені за допомогою інших, наприклад електронна пошта на основі імені користувача. LazyAttribute обробляє такі випадки: він повинен отримати функцію, що приймає створюваний об’єкт і повертає значення для поля:
class UserFactory(DjangoModelFactory):
class Meta:
model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username)
>>>UserFactory()
<User: user1 (user1@example.com)>
>>> # The LazyAttribute handles overridden fields
>>> UserFactory(username='john')
<User: john (john@example.com)>
>>> # They can be directly overridden as well
>>> UserFactory(email='doe@example.com')
<User: user3 (doe@example.com)>
Успадкування
class UserFactory(DjangoModelFactory):
class Meta:
model = base.User
firstname = "John"
lastname = "Doe"
group = 'users'
class AdminFactory(UserFactory):
admin = True
group = 'admins'
Fuzzy attributes
Fuzzy дає змогу генерувати фейкові дані
from factory import fuzzy
...
def setUp(self):
self.username = fuzzy.FuzzyText().fuzz()
self.password = fuzzy.FuzzyText().fuzz()
self.user_id = fuzzy.FuzzyInteger(1).fuzz()
Faker
Faker прийшов на заміну Fuzzy
pip install Faker
from faker import Faker
fake = Faker()
fake.name()
# 'Lucy Cechtelar'
fake.address()
# '426 Jordy Lodge
# Cartwrightshire, SC 88120-6700'
fake.text()
# 'Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi
# beatae sint laudantium consequatur. Magni occaecati itaque sint et sit tempore. Nesciunt
# amet quidem. Iusto deleniti cum autem ad quia aperiam.
# A consectetur quos aliquam. In iste aliquid et aut similique suscipit. Consequatur qui
# quaerat iste minus hic expedita. Consequuntur error magni et laboriosam. Aut aspernatur
# voluptatem sit aliquam. Dolores voluptatum est.
# Aut molestias et maxime. Fugit autem facilis quos vero. Eius quibusdam possimus est.
# Ea quaerat et quisquam. Deleniti sunt quam. Adipisci consequatur id in occaecati.
# Et sint et. Ut ducimus quod nemo ab voluptatum.'
Використання з Factory Boy
from factory.django import DjangoModelFactory
from myapp.models import Book
class BookFactory(DjangoModelFactory):
class Meta:
model = Book
title = factory.Faker('sentence', nb_words=4)
author_name = factory.Faker('name')
Providers
У Faker є велика кількість шаблонів, які розташовані в так званих провайдерах
from faker import Faker
from faker.providers import internet
fake = Faker()
fake.add_provider(internet)
>>> fake.ipv4_private()
'10.10.11.69'
>>> fake.ipv4_private()
'10.86.161.98'
Приклад
factories.py
import factory
from django.contrib.auth import get_user_model
from factory.django import DjangoModelFactory
from app.users.models import Parent, Child
User = get_user_model()
class UserFactory(DjangoModelFactory):
class Meta:
model = User
django_get_or_create = ('email', 'username')
email = factory.Faker('email')
username = email
password = factory.Faker('password')
last_name = factory.Faker('last_name')
first_name = factory.Faker('first_name')
is_email_verified = True
is_active = True
is_superuser = False
is_staff = False
@classmethod
def _create(cls, model_class, *args, **kwargs):
password = kwargs.pop('password')
obj = super()._create(model_class, *args, **kwargs)
obj.set_password(password)
obj.save()
return obj
class SuperUserFactory(UserFactory):
class Meta:
model = User
django_get_or_create = ('email',)
is_superuser = True
is_staff = True
class StaffUserFactory(UserFactory):
class Meta:
model = User
django_get_or_create = ('email',)
is_superuser = False
is_staff = True
class ParentFactory(DjangoModelFactory):
class Meta:
model = Parent
django_get_or_create = ('phone_number',)
user = factory.SubFactory(UserFactory)
address = factory.Faker('address')
post_code = factory.Faker('postcode')
phone_number = factory.Faker('phone_number')
city = factory.Faker('city')
class ChildFactory(DjangoModelFactory):
class Meta:
model = Child
name = factory.Faker('first_name')
parent = factory.SubFactory(ParentFactory)
tests.py
class ChildTestCase(TestCase):
def setUp(self):
self.user = UserFactory()
self.parent = ParentFactory(user=self.user)
self.child_1 = ChildFactory(parent=self.parent)
self.child_2 = ChildFactory(parent=self.parent)
self.child_3 = ChildFactory(parent=self.parent)
self.client.force_login(user=self.user)
Підсумки
Написання тестів не є ні веселощами, ні розвагою і, відповідно, при створенні сайтів часто залишається наостанок (або взагалі не використовується). Але тим не менш, вони є дієвим механізмом, який дає змогу вам переконатися, що ваш код у безпеці, навіть якщо до нього додаються будь-які зміни. Крім того, тести підвищують ефективність підтримки вашого коду.
Домашнє завдання:
- Напишіть тести для login, logout, register у Django.
- Напишіть тести для login, logout, register у DRF.
- Напишіть CRUD тести для Product
- (Завдання до видачі завдання на диплом) Покрийте тестами весь інший код.