#41. Тестирование в Django
Тестирование в Django
План
- Тестирование в Django
- Особенности тестирования
- Client
- Tестирование в DRF
- APITestCase
- APIClient
- Фабричный метод и фабрики
- RequestFactory
- Factory Boy, Fuzzy attributes
- Faker
- Литература
Тестирование в Django
Если вы пишете тесты для веб-приложений, то стоит помнить о важных отличиях в написании и запуске таких тестов.
Подумайте о коде, который нужно протестировать в веб-приложении. Все маршруты, представления и модели требуют много импортов и знаний об используемом фреймворке.
Это похоже на тестирование автомобиля: перед тем, как провести простые тесты, вроде проверки работы фар, нужно включить компьютер в машине.
Django упрощают эту задачу и предоставляют тестовый фреймворк на базе unittest. Вы можете продолжать писать тесты привычным образом, но исполнять их чуть иначе.
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'>)
Для исполнения тестового набора используйте manage.py
test вместо unittest в командной строке:
./manage.py test
Если вам нужно несколько тестовых файлов, замените tests.py на папку tests, положите в нее пустой файл с названием __init__.py
и создайте файлы test_*.py
. Django обнаружит их и выполнит.
Новые Assertions
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)
Остальные в документации
База данных для тестирования
Для тестов используется отдельная база данных, которая будет указана в переменной TEST
в переменной DATABASES
в файле settings.py
:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'USER': 'mydatabaseuser',
'NAME': 'mydatabase',
'TEST': {
'NAME': 'mytestdatabase',
},
},
}
Эта база будет изначально пустая, и будет очищаться после каждого выполненного тест кейса. Что-бы не очишать базу, можно указать параметр --keepdb
(или -k
)
Настрока в PyCharm
Тестирование сайта это сложная задача, потому что она состоит их нескольких логических слоев – от HTTP-запроса и запроса к моделям, до валидации формы и их обработки, а кроме того, рендеринга шаблонов страниц.
Django предоставляет фреймворк для создания тестов, построенного на основе иерархии классов, которые, в свою очередь, зависят от стандартной библиотеки Python unittest
. Несмотря на название, данный фреймворк подходит и для юнит-, и для интеграционного тестирования. Фреймворк Django добавляет методы и инструменты, которые помогают тестировать как веб так и, специфическое для Django, поведение. Это позволяет вам имитировать URL-запросы, добавление тестовых данных, а также проводить проверку выходных данных ваших приложений.
django.test.TestCase класс создает чистую базу данных перед запуском своих методов, а также запускает каждую функцию тестирования в его собственной транзакции. У данного класса также имеется тестовый Клиент, который вы можете использовать для имитации взаимодействия пользователя с кодом на уровне отображения.
Client
Для проведения интеграционного тестирования джанго приложенния нам необходимо отправлять запросы с клиента (браузера), функционал для этого нам предоставлен из коробки, и мы можем им воспользоваться:
>>> 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()
c = Client()
c.login(username='fred', password='secret')
после чего запросы будут приходть от авторизированого пользователя
и метод force_login
принимающий объект юзера, а не логин и пароль.
и метод logout()
GET
>>> c = Client()
>>> response = c.get('/customers/details/', {'name': 'fred', 'age': 7})
отправит GET запрос на
/customers/details/?name=fred&age=7
POST
>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})
И response запишет ответ
отправит 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/'>
Структура тестов
Тестирование view
class AuthorListView(generic.ListView):
model = Author
paginate_by = 10
from django.test import TestCase
from django.contrib.auth.models import User
from catalog.models import Author
from django.urls import reverse
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)
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)
assertContains`(response, 'Christian')
self.assertTemplateUsed(response, 'catalog/author_list.html')
def test_view(self):
response = self.client.get(reverse('authors'), args=[self.author.id])
self.assertEqual(response.status_code, 200)
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)
Так же подерживает форсированую авторизацию
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)
Factory Boy
pip install factory_boy
Прописывание в setUp
создание новых обектов может занимать очень много времени. Что-бы это ускорить, упростить и автоматизировать, можно написать свою фабрику
import factory
from . import base
class UserFactory(factory.Factory):
class Meta:
model = base.User
firstname = "John"
lastname = "Doe"
>>>john = UserFactory()
<User: John Doe>
>>>jack = UserFactory(firstname="Jack")
<User: Jack Doe>
На один класс можно создавать несколько фабрик
class EnglishUserFactory(factory.Factory):
class Meta:
model = base.User
firstname = "John"
lastname = "Doe"
lang = 'en'
class FrenchUserFactory(factory.Factory):
class Meta:
model = base.User
firstname = "Jean"
lastname = "Dupont"
lang = 'fr'
EnglishUserFactory()
<User: John Doe (en)>
>>> FrenchUserFactory()
<User: Jean Dupont (fr)>
Sequences
class UserFactory(factory.Factory):
class Meta:
model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
LazyFunction
При создании можно использовать функции,которые не зависят то класса
class LogFactory(factory.Factory):
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(factory.Factory):
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(factory.Factory):
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
import factory
from myapp.models import Book
class BookFactory(factory.Factory):
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'
Итоги
Написание тестов не является ни весельем, ни развлечением и, соответственно, при создании сайтов часто остается напоследок (или вообще не используется). Но тем не менее, они являются действенным механизмом, который позволяет вам убедиться, что ваш код в находится безопасности, даже если в него добавляются какие-либо изменения. Кроме того, тесты повышают эффективность поддержки вашего кода.