обращение к сообществу открытого исходного кодаGithub
После этого выяснилось, что многие проекты с открытым исходным кодом будут иметь юнит-тесты.TestCase
. Но после поступления на работу я поработал в двух стартапах и обнаружил, что у большинства программистов не выработалась привычка писать юнит-тесты.
Я брал интервью у некоторых программистов в текущей компании, у них в среднем более трех лет опыта работы, но никто из них не имеет привычки писать модульные тесты. спросил"为什么不去编写单元测试呢?"
, не более чем ответ"没有时间"
,"写的都是接口,直接用客户端工具测试一下就可以了"
.
В авторе использованDjango
Рамка идет в комплектеTestCase
Впоследствии было установлено, чтоTestCase
Интерфейс теста не только лучше, чем у некоторых客户端工具
Удобство, но также уменьшает количество изменений после изменения кодаBUG
Вероятность , особенно для некоторых программистов, которые серьезно относятся к чистоте кода и любят оптимизировать код, действительно полезна.
и используя фреймворкTestCase
Напишите модульные тесты и объедините некоторыеCI
инструменты для реализации автоматизированного тестирования, я также напишу специальную статью, чтобы представить мое использованиеGitlab CI
комбинироватьDjango
изTestCase
Несколько советов по внедрению автоматизированного тестирования.
Структура класса TestCase
Не используется для удобстваTestCase
читатели, позвольте мне кратко представитьTestCase
структура класса.
ОбщийTestCase
Зависит отsetUp
функция,tearDown
функция иtest_func
сочинение.
здесьtest_func
это функция, в которой вы написали тестовую логику, иsetUp
функция находится вtest_func
функция, выполняемая перед функцией,tearDown
функция находится вtest_func
Функция для выполнения после выполнения.
from django.test import TestCase
class Demo(TestCase):
def setUp(self):
print('setUp')
def tearDown(self):
print('tearDown')
def test_demo(self):
print('test_demo')
def test_demo_2(self):
print('test_demo2')
Мы можем сделать это с помощьюDjango
Запустите следующую команду в корневом каталоге проекта, чтобы запустить этот модульный тест.
python manage.py test development_of_test_habits.tests.test_demo.Demo
При использованииPycharm
Если вы хотите запустить его, вы можете напрямую щелкнуть стрелку запуска в левой части класса, чтобы запустить его более удобно илиDebug
Это модульный тест.
Вы можете четко увидеть порядок выполнения этого модульного теста по результату после запуска.
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
setUp
test_demo
tearDown
.setUp
test_demo2
tearDown
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Destroying test database for alias 'default'...
Кроме того, из результатов выполнения видно, что модульный тест создает тестовую базу данных перед тестом.
Создание тестовой базы данных для псевдонима «по умолчанию»…
Затем база данных уничтожается в конце теста.
Уничтожение тестовой базы данных для псевдонима «по умолчанию»…
Это унаследованоDjango
в рамкеTestCase
, это уже поможет вам реализовать некоторую логику для тестирования, поэтому нам не нужноsetUp
иtearDown
Эта логика реализована в функции.
Протестируйте интерфейс с помощью TestCase.
Далее поговорим о том, как мы используемTestCase
Для тестирования интерфейса сначала пишем простой интерфейс, здесь автор используетDjango Rest Framework
изAPIView
Чтобы писать, читатели также могут использовать свои собственные методы записи.
from rest_framework.views import APIView
from rest_framework.response import Response
class HelloTestCase(APIView):
def get(self, request, *args, **kwargs):
return Response({
'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',')
})
Затем этот интерфейсный класс добавляется к нашему маршруту.
from django.urls import path
from development_of_test_habits import views
urlpatterns = [
path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'),
]
Далее пишемHelloTestCase
Класс модульного теста для проверки наших тестовых случаев.
from django.urls import resolve, reverse
from django.test import TestCase
class HelloTestCase(TestCase):
def setUp(self):
self.name = 'Django'
def test_hello_test_case(self):
url = '/test_case/hello_test_case'
# url = reverse('hello_test_case')
# Input: print(resolve(url))
# Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[])
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 期望的Http相应码为200
data = response.json()
self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回结果为'Hello , I am a test Case'
response = self.client.get(url, {'name': self.name})
self.assertEqual(response.status_code, 200) # 期望的Http相应码为200
data = response.json()
self.assertEqual(data['msg'], 'Hello Django I am a test Case') # 期望的msg返回结果为'Hello Django I am a test Case'
существуетsetUp
функция, я определяюname
собственность и назначена какDjango
Легко использовать позже.
Интерфейс модульного тестирования в основном разделен на следующее важное содержимое.
Запрошенный адрес маршрутизации
При тестировании интерфейса это не что иное, как инициирование запроса и проверка правильности возвращаемого статуса и содержимого ответа. Сделать запросurl
адрес, есть два способа настроить адрес запроса.
1. Напрямую установить адрес запроса
url = '/test_case/hello_test_case'
2. Черезdjango.urls.reverse
функцию и установить в маршрутеname
чтобы получить запрошенный адрес
url = reverse('hello_test_case')
Здесь, во введении ниже, мы также можем передатьdjango.urls.resolve
иurl
Получите соответствующий класс интерфейса или функцию интерфейса`.
запрашивающий клиент
Для инициации запроса, помимо маршрутизации, нам также нужен клиент, который инициирует запрос. питонrequests
Библиотеки — отличные клиентские инструменты, ноDjango
в егоTestCase
Клиентский инструмент уже интегрирован в класс, нам просто нужно вызватьTestCase
изclient
свойства, чтобы получить клиента.
client = self.client
сделать запрос
Инициация запроса очень проста и требует только одной строки кода, и мы можем получить тело ответа через запрос.
response = self.client.get(url)
Если вам нужно нести параметры, просто передайтеdata
параметр.
response = self.client.get(url, {'name': self.name})
Проверить тело ответа
В модульных тестахTestCase
изassertEqual
несколько похожеpython
изassert
функция, кромеassertEqual
в дополнении кassertNotEqual
,assertGreater
,assertIn
и Т. Д. Здесь я в основном делаю две инспекции, одна для проверкиstatus_code
Равен ли он200
.
self.assertEqual(response.status_code, 200) # 期望的Http相应码为200
Другой — проверить правильность содержания ответа.
data = response.json()
self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回结果为'Hello , I am a test Case'
Это самый простой модульный тест для тестирования запросов, но в реальном интерфейсе нам нужны данные, поэтому нам также нужно сгенерировать тестовые данные.
Вот очень удобная библиотекаmixer
, что упрощает создание тестовых данных в наших модульных тестах.
Используйте микшер для генерации тестовых данных в TestCase
Прежде всего, мы задаем сценарий. Например, мы записали домашнее задание учеников в школьном классе. Нам нужен интерфейс для возврата списка домашних заданий ученика, и этот интерфейс может быть запрошен только после входа пользователя в систему.models
и класс интерфейса выглядит следующим образом.
from django.db import models
class School(models.Model):
name = models.CharField(max_length=32)
class Class(models.Model):
school_id = models.ForeignKey(to=School, on_delete=models.PROTECT)
name = models.CharField(max_length=32)
class Student(models.Model):
class_id = models.ForeignKey(to=Class, on_delete=models.PROTECT)
name = models.CharField(max_length=32)
class HomeWork(models.Model):
student_id = models.ForeignKey(to=Student, on_delete=Student)
name = models.CharField(max_length=32)
Я использую интерфейсDjango rest framework
изReadOnlyModelViewSet
Класс представления реализует функцию возвратаjson
набор результатов иjson
имеютHomeWork
изSchool Name
,Class Name
иStudent Name
, код класса представления и код сериализации выглядят следующим образом.
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated
from development_of_test_habits.models import HomeWork
from development_of_test_habits.serializers import HomeWorkSerializer
class HomeWorkViewSet(ReadOnlyModelViewSet):
queryset = HomeWork.objects.all()
serializer_class = HomeWorkSerializer
permission_classes = (IsAuthenticated, )
from rest_framework import serializers
from development_of_test_habits.models import HomeWork
class HomeWorkSerializer(serializers.ModelSerializer):
class Meta:
model = HomeWork
fields = ('school_name', 'class_name', 'student_name', 'name')
school_name = serializers.CharField(source='student_id.class_id.school_id.name', read_only=True)
class_name = serializers.CharField(source='student_id.class_id.name', read_only=True)
student_name = serializers.CharField(source='student_id.name', read_only=True)
Наконец, добавьте наш интерфейсный класс к маршруту.
urlpatterns = [
path('hello_test_case', views.HelloTestCase.as_view(), name='hello_test_case'),
path('api/home_works', views.HomeWorkViewSet.as_view({'get': 'list'}), name='home_works_list')
]
После завершения написания интерфейса можно приступить к написанию юнит-тестов, определитьHomeWorkAPITestCase
тестовый класс и вsetUp
генерировать тестовые данные.
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from mixer.backend.django import mixer
from development_of_test_habits import models
class HomeWorkAPITestCase(TestCase):
def setUp(self):
self.user = mixer.blend(User)
self.random_home_works = [
mixer.blend(models.HomeWork)
for _ in range(11)
]
Вот введениеmixer
Этот модуль, этот модуль будет случайным образом генерировать тестовые данные на основе модели, которую вы определяете, и полей модели,包括这个数据的外键数据
. Таким образом, нам очень удобно иметь много реляционных данных на этом уровне, иначе нам нужно генерировать данные слой за слоем. использовать в кодеmixer
Создан случайный пользователь и 11 случайныхHomeWork
данные.
Далее напишите логический код для теста.
class HomeWorkAPITestCase(TestCase):
def setUp(self):
self.user = mixer.blend(User)
self.random_home_works = [
mixer.blend(models.HomeWork)
for _ in range(11)
]
def test_home_works_list_api(self):
url = reverse('home_works_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
self.client.force_login(self.user)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data), len(self.random_home_works))
data_fields = [key for key in data[0].keys()]
self.assertIn('school_name', data_fields)
self.assertIn('class_name', data_fields)
self.assertIn('student_name', data_fields)
self.assertIn('name', data_fields)
сначала черезdjango.urls.reverse
Получены имена маршрутов для функций и интерфейсовurl
, первым шагом является проверка пользователя, запрашивающего интерфейс без входа в систему. Ожидаемый код ответа на запрос здесь:403
.
response = self.client.get(url)
self.assertEqual(response.status_code, 403)
мы проходимclient
один из登陆
функцияforce_login
Давайте авторизуемся под нашим случайно сгенерированным пользователем и снова запросим интерфейс, на этот раз желаемый код запроса200
.
self.client.force_login(self.user)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
Наконец, убедитесь, что количество возвращаемых результатов верно, а поля, определенные в результатах, указаны правильно.
data = response.json()
self.assertEqual(len(data), len(self.random_home_works))
data_fields = [key for key in data[0].keys()]
self.assertIn('school_name', data_fields)
self.assertIn('class_name', data_fields)
self.assertIn('student_name', data_fields)
self.assertIn('name', data_fields)
Выше приведен наиболее распространенный процесс тестирования интерфейсов в проекте.
Некоторые проблемы, на которые необходимо обратить внимание при использовании TestCase
Предположим, мы хотим добавить в интерфейс请求头
,отHelloTestCase
Возьмем в качестве примера интерфейс, мы хотим добавитьTEST_HEADER
заголовок запроса, то при логической обработке интерфейса нужно добавить этот заголовок запросаHTTP_
префикс.
class HelloTestCase(APIView):
def get(self, request, *args, **kwargs):
data = {
'msg': 'Hello %s I am a test Case' % request.query_params.get('name', ',')
}
test_header = request.META.get('HTTP_TEST_HEADER')
if test_header:
data['test_header'] = test_header
return Response(data)
Если мы используем клиентские инструменты, такие какPost Man
,RestFul Client
Подождите, просто добавьте в заголовок запроса при запросеTEST_HEADER
Вот и все. Но в модульном тестировании нам также нужно поставитьHTTP_
Этот префикс добавляется, иначе логику интерфейса не получить.
def test_hello_test_case(self):
url = '/test_case/hello_test_case'
# url = reverse('hello_test_case')
# Input: print(resolve(url))
# Output: ResolverMatch(func=development_of_test_habits.views.hello_test_case.HelloTestCase, args=(), kwargs={}, url_name=hello_test_case, app_names=[], namespaces=[])
response = self.client.get(url)
self.assertEqual(response.status_code, 200) # 期望的Http相应码为200
data = response.json()
self.assertEqual(data['msg'], 'Hello , I am a test Case') # 期望的msg返回结果为'Hello , I am a test Case'
response = self.client.get(url, {'name': self.name})
self.assertEqual(response.status_code, 200) # 期望的Http相应码为200
data = response.json()
self.assertEqual(data['msg'], 'Hello Django I am a test Case') # 期望的msg返回结果为'Hello Django I am a test Case'
# 假设我们要在接口中增加请求头'TEST_HEADER'
# 则在测试时需要加上前缀'HTTP_'最终的结果为'HTTP_TEST_HEADER'
response = self.client.get(url, **{'HTTP_TEST_HEADER': 'This is a test header.'})
data = response.json()
self.assertEqual(data['test_header'], 'This is a test header.')
Суммировать
После использования тест-кейсов для тестирования интерфейса, автор начал вырабатывать привычку напрямую использовать модульные тесты для тестирования интерфейса после написания.Это не только для объяснения функции моего интерфейса другим, но и для уменьшения количества ошибок в Помогите, я надеюсь, что читатели смогут и дальше развивать хорошую привычку писать модульные тесты таким образом.
本人博客原文地址:elfgzp.cn/2018/12/07/…