#40. Тестування у Django, DRF

Тестування у Django, DRF

План

  1. Тестування в Django
    1. Особливості тестування
    2. Client
  2. Тестування в DRF
    1. APITestCase
    2. APIClient
  3. Фабричний метод і фабрики
    1. RequestFactory
    2. Factory Boy, Fuzzy attributes
    3. Faker
  4. Література

Види тестування

Тестувальники діляться на 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

Документ, що описує сукупність кроків, конкретних умов та параметрів, необхідних для перевірки реалізації функції, що тестується чи її частини.

A cartoon for peanut butter and jelly sandwich tests

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)

Підсумки

Написання тестів не є ні веселощами, ні розвагою і, відповідно, при створенні сайтів часто залишається наостанок (або взагалі не використовується). Але тим не менш, вони є дієвим механізмом, який дає змогу вам переконатися, що ваш код у безпеці, навіть якщо до нього додаються будь-які зміни. Крім того, тести підвищують ефективність підтримки вашого коду.

Домашнє завдання:

  1. Напишіть тести для login, logout, register у Django.
  2. Напишіть тести для login, logout, register у DRF.
  3. Напишіть CRUD тести для Product
  4. (Завдання до видачі завдання на диплом) Покрийте тестами весь інший код.

Література

  1. Тестування додатків Django від mozilla
  2. Web-додаток із прикладами тестів