Заявление об авторских правах: Автор «Ян Бэй (WeChat)
shin-devops
)",опубликовано"Наггетс», несанкционированное воспроизведение запрещено!
Модульное тестирование является важным методом обеспечения качества при разработке программного обеспечения.
С помощью модульного тестирования вы можете «сначала протестировать» и внедрить TDD; вы также можете гарантировать, что исходная логика не будет затронута при рефакторинге кода.
В официальной документации Django "контрольная работа«В главе более подробно описано, как выполнить модульное тестирование, цель этой статьи — «Расскажите, как написать базовый класс модульного тестирования, заняв как можно меньше места.", а также некоторые расширенные возможности (такие какMock
) практиковаться, делая написание одного теста проще, чем мучительнее.
официальная документация
Основное использование
Модульное тестирование вызывает метод (выполнение), чтобы определить, является ли эффект метода ожидаемым (утверждение), что является процессом выполнения и оценки результата.
Тогда следующий код будет легче понять:
from django.test import TestCase
from myapp.models import Animal
class AnimalTestCase(TestCase):
def test_animals_can_speak(self):
"""Animals that can speak are correctly identified"""
Animal.objects.create(name="lion", sound="roar")
Animal.objects.create(name="cat", sound="meow")
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
self.assertEqual(lion.speak(), 'The lion says "roar"')
self.assertEqual(cat.speak(), 'The cat says "meow"')
Пример взят с официального сайта Django.
вtest_
в тестовом методе в начале, вызвав функцию, а затем используяassert
результат метода.
Если есть процесс подготовки среды и восстановления тестовых данных, то его можно использоватьsetUp
иtearDown
способ обработки:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
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):
"""Animals that can speak are correctly identified"""
lion = Animal.objects.get(name="lion")
cat = Animal.objects.get(name="cat")
self.assertEqual(lion.speak(), 'The lion says "roar"')
self.assertEqual(cat.speak(), 'The cat says "meow"')
def tearDown(self):
Animal.objects.filter(name__in=["lion", "cat"]).delete()
запустить тест
python manage.py test
При запуске одного теста добавьте--keepdb
параметры, чтобы избежать необходимости перестраивать базу данных каждый раз, когда выполняется один тест, и повысить скорость выполнения:
python manage.py test --keepdb
тест интерфейса
Для тестирования интерфейса обычно сама веб-инфраструктура интегрирует «набор тестов» для выполнения модульных тестов путем имитации запросов. Джанго реализовалRequestFactory
класс, который можно использовать непосредственно для отправки запросов:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
from django.contrib.auth.models import User
from django.test import RequestFactory, TestCase
from .views import MyView
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
# Use this syntax for class-based views.
response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200)
APITestCase в DRF
Видно, что метод построения запроса все еще довольно хлопотный, в каждом варианте использования нам нужно инициализироватьRequestFactory
объект при вызове (response = MyView.as_view()(request)
) недостаточно интуитивен.
существуетDjang Rest Framework
Эта проблема решается вAPITestCase
серединаself.client
послать запрос:
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 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')
Пример с официального сайта DRF
Решайте и оптимизируйте проблемы
Итак, нам нужно подумать, соответствуют ли эти методы Django и DRF нашим потребностям? Есть ли более простой способ сделать это?
Вопрос 1. Как имитировать состояние входа пользователя
В нашем проекте есть контроль разрешений для пользователей, поэтому первая возникшая проблема — «как смоделировать состояние входа пользователя», чтобы не было ошибок в логике, связанной с разрешениями.
В примере Django, поскольку сначала создается запрос, а затем явноrequest.user
Настроен на созданиеUser
объект для достижения, но в DRF, поскольку процесс построения запроса инкапсулирован, этот метод больше нельзя использовать, что является проблемой, которую необходимо решить.
Решение
Решая эту проблему, я искал на GitHub проекты Django с большим количеством звезд и узнал, как разные проекты оптимизируют модульные тесты.
вSentryупаковалlogin_asметод обхода входа пользователя путем добавления информации о пользователе в текущий сеанс.
По этой идее я упростил код реализации Sentry и получил следующий метод:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.models import AnonymousUser
from django.utils.functional import cached_property
from django.http import HttpRequest
from rest_framework.test import APITestCase as BaseAPITestCase
class APITestCase(BaseAPITestCase):
@staticmethod
def create_session():
engine = import_module(settings.SESSION_ENGINE)
session = engine.SessionStore()
session.save()
return session
@cached_property
def session(self):
return self.create_session()
def save_session(self):
self.session.save()
self.save_cookie(
name=settings.SESSION_COOKIE_NAME,
value=self.session.session_key,
expires=None
)
def save_cookie(self, name, value, **params):
self.client.cookies[name] = value
self.client.cookies[name].update({
k.replace('_', '-'): v
for k, v in params.items()
})
def login(self, user):
"""登录用户,用于通过权限校验"""
user.backend = settings.AUTHENTICATION_BACKENDS[0]
request = self.make_request()
login(request, user)
request.user = user
self.save_session()
def make_request(self, user=None, auth=None, method=None):
request = HttpRequest()
if method:
request.method = method
request.META['REMOTE_ADDR'] = '127.0.0.1'
request.META['SERVER_NAME'] = 'testserver'
request.META['SERVER_PORT'] = 80
request.REQUEST = {}
# order matters here, session -> user -> other things
request.session = self.session
request.auth = auth
request.user = user or AnonymousUser()
request.is_superuser = lambda: request.user.is_superuser
request.successful_authenticator = None
return request
Перед отправкой запроса, позвонивlogin
Метод сохраняет сеанс после имитации входа в систему, так что вызовself.client
Принесите SessionId при отправке запроса для достижения эффекта входа в систему:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
class MyViewTest(APITestCase):
def setUp(self) -> None:
self.user = self.create_user(is_staff=True)
self.login(self.user)
def test_get_myview_details(self) -> None:
# 假设 /api/myview 只有在 is_staff 用户登录情况下才可请求
response = self.client.get(path=/api/myview)
# status_code 不为 401,说明用户已经登录
self.assertEqual(response.status_code, 200)
Вопрос 2. Как легко инициализировать и подготовить данные
Код логики интерфейса обычно опирается на существующие данные для операций чтения и записи. «Данные подготовки к инициализации» должны быть более гибкими, уменьшая инициализацию свойств, не связанных с этим тестовым примером, и в то же время сохраняя операции. выполняется с помощью варианта использования ясно.
Напишите несколько методов create_ со значениями по умолчанию.
При инициализации некоторых данных в модульном тесте мы обычно хотим настроить только некоторые поля, поэтому мы можем инкапсулировать все методы создания модели данных в набор тестов и автоматически добавлять значения по умолчанию для всех полей:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
from django.contrib.auth.models import User
from uuid import uuid4
from rest_framework.test import APITestCase as BaseAPITestCase
class APITestCase(BaseAPITestCase):
@staticmethod
def create_user(username=None, **kwargs):
if username is None:
username = uuid4().hex
return User.objects.create_user(username=username, **kwargs)
Лучший способ — извлечь класс, предназначенный для обработки данных инициализации, который будет выглядеть лучше и его будет проще поддерживать:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
from rest_framework.test import APITestCase as BaseAPITestCase
class Factories(object):
@staticmethod
def create_user(username=None, **kwargs):
...
@staticmethod
def create_task(task_name=None, **kwargs):
...
class APITestCase(Factories, BaseAPITestCase):
pass
Обработка URL-адресов
URL-адрес может быть записан в формате пути, но если путь изменен, его сложнее поддерживать.
Маршрутизация в Django поддерживает поиск обратного пути через Endpoint:
>>>from django.urls import reverse
>>>reverse("app_label.endpoint")
/api/my-endpoint
мы вAPITestCase
добавлен классapp_label
иendpoint
два свойства, обеспечивающиеget_url
Легко позвонить:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
class APITestCase(Factories, BaseAPITestCase):
# django App 名
app_label = 'my_app'
# 端点,用于标识 URL
endpoint = None
def get_url(self, *args, **kwargs):
return reverse(f"{self.app_label}:{self.endpoint}", args=args, kwargs=kwargs)
В тестовом случае:
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
class TaskDetailTest(APITestCase):
endpoint = 'task-detail'
def setUp(self) -> None:
self.url = self.get_url(task_id=self.task.pk)
...
def test_get_task_details(self):
result = self.client.get(self.url)
...
Вопрос 3. Как выполнить асинхронную задачу без запуска службы
И тогда мы используем сельдерей для выполнения асинхронных задач, «как выполнять асинхронные задачи без запуска службы»?
Решение 1. Синхронное выполнение асинхронного кода
Изменив конфигурацию Celery:
class MyTest(TestCase):
def setUP(self):
celery.conf.update(CELERY_ALWAYS_EAGER=True)
После модификации асинхронные задачи будут выполняться синхронно, и нет необходимости запускать такие сервисы, как Celery Worker и RabbitMQ. Следующие два эквивалентны:
add.delay(2, 2)
add(2, 2)
Но у этого способа есть недостаток, то есть количество тестовых случаев станет количеством ветвей задачи Celery * количеством условных ветвей основной задачи, которые будут умножаться.
Следовательно, необходимо использовать Mock для уменьшения сценариев перекрестного покрытия.
Решение 2: Макет
Предположим, что код выглядит следующим образом:
tasks.py
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
@celery_task
def add(x, y):
print(x + y)
def main_func():
x, y = do_something()
add.delay(x, y)
тогда вы можете использоватьMock
будет метод асинхронной задачиdelay
пропустить, дойти только до тестаmain_func
другой код в методе;
потому что пропустилadd
метод, то количествоadd
метод проверки:
test.py
"""
@Author: Shin Yang
@WeChat: shin-devops
"""
from unittest import mock
from tasks import add, main_func
class MyTest(TestCase):
@mock.patch("tasks.add.delay")
def test_main_func(self, mocked_delay):
mocked_delay.return_value = None
result = main_func()
mocked_delay.assert_called_with(1, 2)
self.assertEqual(result, my_expect1)
def test_add(self):
result = add()
self.assertEqual(result, my_expect2)
Суммировать
Возвращаясь к началу, «модульное тестирование является важным методом обеспечения качества при разработке программного обеспечения», но многие компании/команды/разработчики игнорируют модульное тестирование ради «вывода», видимого невооруженным глазом.
Модульное тестирование — это долгосрочное вложение, написание модульных тестов при разработке требований может показаться написанием двух кусков кода, но на самом деле для компаний наличие однократно протестированного кода может снизить вероятность рисков в процессе сопровождения кода, и для решения этих проблем Потери, вызванные рисками; для разработчиков осведомленность о качестве и мышление в области тестирования могут позволить вам учитывать удобство сопровождения и тестируемость большего количества кода во время разработки и писать более качественный код.
Ссылаться на
- docs.Django project.com/this-functions/2.2…
- woohoo. Django-rest-framework.org/api-expensive/he…
- GitHub.com/gets entry/ — это…
Заявление об авторских правах: Автор «Ян Бэй (WeChat)
shin-devops
)",опубликовано"Наггетс», несанкционированное воспроизведение запрещено!