Опыт Django (2) Использование TestCase для тестирования интерфейса

Django

обращение к сообществу открытого исходного кода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Функция для выполнения после выполнения.

development_of_test_habits/tests/test_demo.py view raw
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Это модульный тест.
img

Вы можете четко увидеть порядок выполнения этого модульного теста по результату после запуска.

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Чтобы писать, читатели также могут использовать свои собственные методы записи.

development_of_test_habits/views/hello_test_case.py view raw
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', ',')
        })

Затем этот интерфейсный класс добавляется к нашему маршруту.

development_of_test_habits/urls.py view raw
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Класс модульного теста для проверки наших тестовых случаев.

development_of_test_habits/tests/test_hello_test_case.py view raw
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и класс интерфейса выглядит следующим образом.

development_of_test_habits/models.py view raw
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, код класса представления и код сериализации выглядят следующим образом.

development_of_test_habits/views/api/home_work.py view raw
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, )
development_of_test_habits/serializers.py view raw
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)

Наконец, добавьте наш интерфейсный класс к маршруту.

development_of_test_habits/serializers.py view raw
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генерировать тестовые данные.

development_of_test_habits/tests/test_home_works_api.py viewraw
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данные.

Далее напишите логический код для теста.

development_of_test_habits/tests/test_home_works_api.py viewraw
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_префикс.

development_of_test_habits/views/hello_test_case.py view raw
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_Этот префикс добавляется, иначе логику интерфейса не получить.

development_of_test_habits/tests/test_hello_test_case.py view raw
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/…