Для разработчиков, которые хотят узнать все, что им нужно, чтобы стать эффективными и эффективными тестерами, у которых нет команды QA позади них или что просто хочет взять контроль над своим качеством кода.
Тестирование является ключевым компонентом жизненного цикла кода разработчика, будь то для всех команд или для одиноких волков, но всесторонние руководства по тестированию не сдерживались ростом рамок Django REST, и нет ресурса, у меня есть шанс увидеть там, что охватывает все Я думаю, что стандарт тестирования должен сделать для приложения. То есть:
- Имейте достаточно быстрые тесты для эффективных кодовых тестов для разработчиков и для трубопроводов CI/CD.
- Имейте достаточно тесты, чтобы изолировать, что ломается в кусочке кода и какие перерывы во внешнем/внутреннем коде, который мы тестируем, зависит от.
- Оставьте четкий набор полезных инструментов, чтобы определить соответствующие индивидуальные или несколько тестов, приводящие к ракетно-быстрой рабочему процессу для разработчика.
В этой статье я предлагаю провести тестирующий подход, структуру для тестов, набор инструментов для тестирования при разработке, а также показываю, как тестировать каждую общему часть приложения Django. Это работа в процессе, и я ищу обратной связи и предложения, сын любой значимый комментарий поможет тонны.
- Меньшее котельная (вам не нужно запоминать заявления о том, как с
Unittest
) - Настраиваемые данные об ошибке
- Авто-открытие независимо от используемого IDE
- Маркеры для по стандартизации команды
- Удивительные команды терминалов
- Параметрирование для сухого
- Огромные плагины Сообщество Все это и больше сделают из навыка шанжеров игр, чтобы быть профессиональным тестером Proficint PiTest.
Это хорошая идея для обеспечения масштабных приложений для создания Тесты
Папка и внутри она делают папку для каждого из приложений в нашем проекте Django:
... ├── tests │ ├── __init__.py │ ├── test_app1 │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── factories.py │ │ ├── e2e_tests.py │ │ ├── test_models.py <-- │ │ ├── test_signals.py <-- │ │ ├── test_serializers.py │ │ ├── test_utils.py │ │ ├── test_views.py │ │ └── test_urls.py │ │ │ └── ... └── ...
Конфигурации настроек Pтостесте могут быть установлены в Pytest.ini
Файлы под [pytest]
или в setup.cfg
Файлы под [Инструмент: pytest]
:
:in pytest.ini [pytest] DJANGO_SETTINGS_MODULE = ... markers = ... python_files = ... addopts = ... :in setup.cfg [tool:pytest] DJANGO_SETTINGS_MODULE = ... markers = ... python_files = ... addopts = ...
Во многих руководствах вы увидите людей, создающих Pytest.ini
файлы. Эти файлы могут быть использованы только для настроек Ptyest, и мы захочем использовать еще много настроек, таких как настройки плагинов, и не имеют необходимости добавлять множество файлов для каждого плагина, поскольку наш проект растет. Так что настоятельно рекомендуется идти вместо этого с setup.cfg
Файл Insead.
В этих файлах основная конфигурация, которая будет иметь значение для вас:
Django_settings_module
: Указывает, где в рабочем режиме расположены настройки.python_files
: Указывает, что узоры Pтойцы будут использовать для соответствия тестовым файлам.Дополнения
: Указывает, что аргументы командной строки Pтойцы должны работать с каждым, когда мы работаем Pteest.Маркеры
: Здесь мы определяем маркеры, которые мы, и наша команда, впустя, согласится использовать для категорийных тестов (I.E: «Устройство», «Интеграция», «E2E», «регрессия» и т. Д.).
Хотя в мире в мире существуют много разных имен для всех возможных тестов посмотреть здесь , мы можем сойти с тщательно разработчиком приложение, зная следующие 4 типа тестов:
- Агрегаты тесты : Испытание соответствующего куска кода, изолированных (в основном путем издевательства внешнего кода к тестированному коду) от взаимодействия с другими единицами кода. Будьте его внутренним кодом, как функция помощника, которую мы сделали, чтобы очистить код, вызов базы данных или вызов на внешний API.
- Интеграционные тесты : Испытания, которые тестируют кусок кода, не выделяя их от взаимодействия с другими блоками.
- E2E тесты : E2E обозначает «конец до конца», эти тесты являются тестами интеграции, которые тестируют конец к концу потока приложения Django, которые мы тестируем.
- Регрессионные тесты : А своего рода тест, будь то интеграция или блок, был возник путем ошибки, которая была покрыта сразу после исправления его тестом, чтобы ожидать его в будущем.
Прежде чем приступить к порекомендует пошаговый подход к пуленепробиваемому вашему приложению с тестами, это хорошая идея для Stablish, почему мы тестируем. Есть главным образом две причины:
- Вести нас во время кодирования : Хотя мы разрабатываемся, у нас будут эти тесты, чтобы направить наш код, целью кода, которую мы строим, будут передавать это тесты.
- Чтобы убедиться, что мы ничего не нарушили : Каждый кусок кода похож на лапшу спагетти на блюде спагетти. Шансы вы сломали что-то при принятии лапши и положив его обратно потом, по крайней мере, говорят наименее.
Существует два основных тестирования кода для тестирования разработчиков:
- TDD (Тестовое развитие): создание тестов, прежде чем утверждать их через код.
- Тестирование после разработки : Тестирование куска кода сразу после его создания.
Если мы не будем очень ознакомиться с синтаксисом и кодом, мы ожидаем написать (почти невозможно, когда мы еще не уверены в коде, мы будем писать), это почти всегда хорошая идея, чтобы пойти с последним. То есть, чтобы написать наш код и после того, как мы рассмотрим его закончить, перейдите к тестируемую, как ожидалось. Но я рекомендую их сочетание:
- Сделайте сквозные тесты : Это должен быть компонент TDD нашего конечного тестового номера. Эти тесты помогут вам в первую очередь и прежде всего для макета.
- Создание модульных тестов : Напишите код и создайте изолированные тесты для каждой из частей приложения Django. Это не только поможет разработчику посредством процесса разработки, но также будет чрезвычайно полезным, чтобы определить, где происходит проблема.
Единственные интеграционные тесты в нашем тестовом наборе должны быть E2E. Эти тесты не должны быть частью тестирования нашей команды, в то время как разрабатывающиеся и, когда сталкиваются с большими кодовыми базами, учитывая тесты в интеграции, на которых нет времени, это даже не должно формировать часть конвейера CI/CD, который мы устанавливаем. E2E Tests в основном следует использовать в качестве здравоохранения, которую мы разработчики работают после всех устройств тестов подразделения кода, которые мы работаем над пройденным (| », см. Pтойские документы, говорящие об этой стратегии ), чтобы убедиться, что кода сплочена среди различных Кодовые единицы работает должным образом.
Еще одна причина, чтобы избежать тестов E2E на ваших трубопроводах и рабочую процессу разработчика, и используя вместо модульных тестов, состоит в том, чтобы избежать флокистых тестов, тесты, которые не выполняются при запуске в номере, но это отлично работает при запуске в одиночестве. Посмотрите, как исправить Flaky Tests Отказ
Тесты, которые мы должны использовать в нашем рабочем процессе, действительно, тесты модуля, которые мы могли бы идентифицировать с Petest Marker Или в самом названии функции тестирования, чтобы позже запустить их все с одной командой. Эта команда будет часто использоваться во время нашего рабочего процесса развития.
Проблема, которая может возникнуть на главе читателя, заключается в том, что следуя этот подход, мы не можем проверить модели. Это на самом деле, что я собираюсь случиться. Хотя это не статья о том, как управлять вашей бизнес-логикой внутри разных частей вашего приложения Dango, модели должны использоваться только для представления данных. Логика, которую мы можем понадобиться для модели, должна храниться в любой сигналы или Модель менеджеров Отказ
Поскольку мы избегаем доступа к базе данных для наших основных испытаний, мы оставим работу, чтобы мы правильно создали наши модели к тестам E2E.
Хотя это может быть сложный вопрос на некоторых других языках и структурах программирования, зная, что такое тестируемая единица в Django, прост, поскольку мы можем классифицировать нашу логику в 6 частях. Таким образом, тест на единицу может быть:
- Модель (методы модели/модельные менеджеры)
- Сигнал
- Сериализатор
- Helper Object A.K.A “UTILS” (Функции, классы, метод и т. Д.)
- Просмотр/просмотр
- URL-конфигурация
Маркеры
Перед введением примеров испытаний, это хорошая идея ввести некоторые полезные маркеры Pteest предоставляет нам. Маркеры – это просто декораторы с форматом @ pytest.mark. <маркер>
Мы устанавливаем обертывание наших тестовых функций.
@ pytest.mark.parrametrize ()
: Этот маркер будет использоваться для проведения одного и того же теста несколько раз с разными значениями, работающими в качестве контура.@ pytest.mark.django_db
Если мы не даем тестовый доступ к БД, по умолчанию он не сможет получить доступ к БД. Этот маркер, предоставленный pytest-django Плагин, конечно, имеет смысл только для интеграционных испытаний.
Издеваться
При изготовлении тестов подразделения мы захотим издеваться на доступ к внешним API к БД и внутреннему коду. Вот где будут полезны следующие библиотеки:
-
Pytest-Mock
: обеспечитьUnittest.Mock
Объекты, такие как объект Mock и неинвазивный патч функционируют через приспособление Mocker. -
Запрос-макет
: обеспечить Запросы на фабрику черезРФ
Приспособление, а также способность макировать запросы объектов. -
Django-Mock-queries
: Предоставляет возможность издеваться над объектом запроса и заполнить его нежистыми экземплярами объектов.
Команды Pteest
Команда, которую мы запустим в нашем рабочем каталоге, чтобы запустить все тесты, это pteest
, но Pytest предоставляет команды, которые помогут нам, среди прочего, чтобы сузить тесты, которые мы работаем и легко выбираем только тесты, которые мы хотим запустить. Основные:
-k <выражение>
: Соответствует имя файла, класса или функции внутри папки тестов, которые содержат указанное выражение.-M
: будет запустить все тесты с введенным маркером.-М "не <маркер>"
: Запустит все тесты, которые не имеют введенного маркера.-x
: Перестает проводить тесты после того, как тест не удается, позволяя нам остановить пробежку прямо там, чтобы мы могли вернуться к отладке нашего теста вместо того, чтобы ждать тестовой набор для завершения работы.--lf
: Начинает запустить тестовый набор из последнего неудачного теста, идеально подходит для того, чтобы избежать непрерывных тестов, которые мы уже знаем проход при отладке.-vv
: Показывает более подробную версию неудачного утверждения.- Гов
: Покажите% тестов, охватываемых тестами (зависит отPytest-COV
плагин).--Рунс
: используется для борьбы с Flacy Tests Испытания, которые не удаются при запуске в тестовом наборе, но проходят при запуске в одиночку.
Поддоны
ADDOPTS – это команды pteest, которые захотят запускать каждый раз, когда мы запускаем pteest
команда Поэтому нам не нужно вводить его каждый раз.
Мы можем настроить наши дополнительные вершины следующим образом:
DJANGO_SETTINGS_MODULE = ... markers = ... python_files = ... addopts = -vv -x --lf --cov
Испытания точны
Чтобы быстро запустить только желаемый тест, мы можем запустить pteest
-k
Команда и введите нужное имя теста.
Если мы хотим вместо этого запустить набор тестов с чем-то общей, мы можем запустить pteest
используя -k
Команда, чтобы выбрать все Тест _ *. py
Файлы, все Тест *
классы или все test_ *
Функции с вставленным выражением.
Хороший путь к групповым тестам, это установить пользовательские маркеры в нашу Pytest.ini
/ setup.cfg
Файл и поделитесь маркерами в нашей команде. Мы можем иметь маркер для каждого вида шаблона, который мы хотим использовать в качестве фильтра для нашего теста.
Какие маркеры актуальны для достижения критериев разработчиков, но то, что я предлагаю использовать маркеры только для группировки тестов, которые трудно группировать подходящие выражения, используя -k
команда. Пример общего соответствующего маркера – единица
Маркер, чтобы выбрать только модульные тесты.
На нашем Pytest.ini
Мы поставим маркер так:
[tool:pytest] markers = # Define our new marker unit: tests that are isolated from the db, external api calls and other mockable internal code.
Поэтому, если мы хотим отметить тест, который следует за шаблоном теста устройства, мы можем пометить так:
import pytest @pytest.mark.unit def test_something(self): pass
Поскольку у нас будет более одного теста на единицу теста в каждом файле (и если вы будете следовать за моим советом, большинство тестовых файлов будут фактически быть единичными тестами файлов), мы можем избежать потенциала сделать маркер для каждой функции, установив глобальный маркер Для всего файла, объявляя переменную PyteStmark в верхней части файла сразу после импорта, который будет содержать сингулярное Pтойное маркер или список маркеров:
# (imports) # Only one global marker (most commonly used) pytestmark = pytest.mark.unit # Several global markers pytestmark = [pytest.mark.unit, pytest.mark.other_criteria] # (tests)
Если мы хотим пойти дальше и установить глобальный маркер для всех испытаний, Pytest создает приспособления, называемые предметы
Это представляет все тестовые объекты Pтоиды внутри содержащего каталог. Таким образом, мы могли бы использовать это для создания, например, все
Маркер и отметьте все тестовые файлы на одном уровне и ниже Conftest.py
С этим как это:
# in conftest.py def pytest_collection_modifyitems(items): for item in items: item.add_marker('all')
Заводы
Заводы представляют собой предварительно заполненные экземпляры модели. Вместо того, чтобы вручную совершать экземпляры модели вручную, фабрики сделают для нас работу. Основные модули для создания заводов являются factory_boy
и Model_Bakery
Отказ Для производительности мы должны почти всегда идти с Model_Bakery
, который будет только с учетом модели Django, создать экземпляр модели, заполненного действительными данными. Проблема с Model_Bakery заключается в том, что она создает рандомизированные чепуха данных, поэтому, если мы хотим фабрику, которая генерирует поля, которые не являются случайными персонажами и имеют любой смысл, то мы должны идти вперед и использовать Factory_Boy вдоль гибели, например, если у нас есть Поле «Имя», генерировать имя, которое выглядит как имя, а не случайные буквы.
Для создания фабрик мы захочем создавать и хранить их при создании интеграционных тестов, но мы будем не Хотите хранить их при создании модульных тестов, поскольку мы не хотим получить доступ к БД. Экземпляры, которые сохраняются в БД, называются «постоянными экземплярами» в отличие от «не настойчивых», которые мы будем использовать для того, чтобы издеваться от ответа вызовов баз данных. Вот пример обоих с Model_Bakery
:
from model_bakery import baker from apps.my_app.models import MyModel # create and save to the database baker.make(MyModel) # --> One instance baker.make(MyModel, _quantity=3) # --> Batch of 3 instances # create and don't save baker.prepare(MyModel) # --> One instance baker.prepare(MyModel, _quantity=3) # --> Batch of 3 instances
Если мы хотим иметь прочее, чем случайные данные (например, чтобы показать в интерфейсе к владельцу продукта), мы могли бы либо перезаписать поведение по умолчанию Model_Bakery ( см. Здесь ). Или мы можем написать заводы с factory_boy
и Faker
следующим образом:
# factories.py import factory class MyModelFactory(factory.DjangoModelFactory): class Meta: model = MyModel field1 = factory.faker.Faker('relevant_generator') ... # test_something.py # Save to db MyModelFactory() # --> One instance MyModelFactory.create_batch(3) # --> Batch of 3 instances # Do not save to db MyModelFactory.build() # --> One instance MyModelFactory.build_batch() # --> Batch of 3 instances
Faker имеет генераторы для генерации случайных соответствующих данных для многих различных тем, каждая тема представлена в поставщике факера. Фабрика Boy’s Faker Faker Versa со всеми провайдерами из коробки, так что вы просто должны идти здесь Посмотрите все провайдеры и выберите провайдер, который вам нравится пройти имя генератора, которое вы хотите, чтобы строка для создания поля (I.E: Peaker. Faker («Имя»)
).
Заводы могут быть сохранены либо в Conftest.py
, который является файлом, где мы также можем установить конфигурации для всех тестов на одном уровне каталога, а ниже (например, вот были определены приспособления), или, если у нас есть много разных фабрик для нашего приложения, мы можем хранить их непосредственно в Per-App фабрики файл:
.... ├── tests │ ├── __init__.py │ ├── test_app1 │ │ ├── __init__.py │ │ ├── conftest.py <-- │ │ ├── factories.py <-- │ │ ├── e2e_tests.py │ │ ├── test_models.py │ │ ├── test_signals.py │ │ ├── test_serializers.py │ │ ├── test_utils.py │ │ ├── test_views.py │ │ └── test_urls.py │ │ │ └── ... └── ...
Прежде всего, как указано ранее, тесты должны находиться внутри файла с узорами, указанными в нашем Pytest.ini
/ setup.cfg
файл.
Для упорядочения тестов внутри нашего файла для каждого блока протестированного кода (например, APIView Django) создать класс с тестом, в корпусе верблюда и внутри, которые создают тесты для этого устройства (например, тест для всех принятых методов по мнению).
Для организации теста по функции тестирования я рекомендую использовать критерии «AMAA», пользовательскую версию критериев «AAA». То есть тесты должны следовать этому порядку:
- Организовать : Установите все необходимое для теста
- Макет : издеваться, что нужно было изолировать ваш тест
- Акт : Запустите свой код код.
- Утверждать : утверждать, что результат точно так же, как и ожидалось, чтобы избежать каких-либо неприятных сюрпризов позже.
Поэтому тестовая структура должна выглядеть так:
# tests/test_app/app/test_some_part.py ... # inside test_something.py class TestUnitName: def test_(self): # Arrange # Mock # Act # Assert ...
Для примеров мы будем использовать следующие модели транзакции и валюты для создания примеров вокруг них:
# inside apps/app/models.py import string from django.db import models from django.utils import timezone from hashid_field import HashidAutoField from apps.transaction.utils import create_payment_intent, PaymentStatuses class Currency(models.Model): """Currency model""" name = models.CharField(max_length=120, null=False, blank=False, unique=True) code = models.CharField(max_length=3, null=False, blank=False, unique=True) symbol = models.CharField(max_length=5, null=False, blank=False, default='$') def __str__(self) -> str: return self.code class Transaction(models.Model): """Transaction model.""" id = HashidAutoField(primary_key=True, min_length=8, alphabet=string.printable.replace('/', '')) name = models.CharField(max_length=50, null=False, blank=False) email = models.EmailField(max_length=50, null=False, blank=False) creation_date = models.DateTimeField(auto_now_add=True, null=False, blank=False) currency = models.ForeignKey(Currency, null=False, blank=False, default=1, on_delete=models.PROTECT) payment_status = models.CharField(choices=PaymentStatuses.choices, default=PaymentStatuses.WAI, max_length=21) payment_intent_id = models.CharField(max_length=100, null=True, blank=False, default=None) message = models.TextField(null=True, blank=True) @property def link(self): """ Link to a payment form for the transaction """ return settings.ALLOWED_HOSTS[0] + f'/payment/{str(self.id)}'
И внутри Тесты/test_app/conftest.py
Мы установим наши фабрики в качестве приборов для последующего доступа к ним в качестве параметра в наших тестовых функциях. Не всегда нам нужна экземпляры модели со всеми его полями, заполненными пользователем, возможно, мы хотим автоматически заполнить их на нашей бэкэнде. В этом случае мы можем сделать заказные фабрики с определенными полями заполненными:
def utbb(): def unfilled_transaction_bakery_batch(n): utbb = baker.make( 'transaction.Transaction', amount_in_cents=1032000, # --> Passes min. payload restriction in every currency _fill_optional=[ 'name', 'email', 'currency', 'message' ], _quantity=n ) return utbb return unfilled_transaction_bakery_batch @pytest.fixture def ftbb(): def filled_transaction_bakery_batch(n): utbb = baker.make( 'transaction.Transaction', amount_in_cents=1032000, # --> Passes min. payload restriction in every currency _quantity=n ) return utbb return filled_transaction_bakery_batch @pytest.fixture def ftb(): def filled_transaction_bakery(): utbb = baker.make( 'transaction.Transaction', amount_in_cents=1032000, # --> Passes min. payload restriction in every currency currency=baker.make('transaction.Currency') ) return utbb return filled_transaction_bakery
E2E тесты
Как только у нас уже есть наши модели, и их заводские фабрики, это хорошая идея, чтобы иметь как репетитор, так и в качестве здравомыслие, конец для окончания тестов потоков, которые мы хотим происходить в нашем приложении. Это не должно и, безусловно, не пройдет, как только мы сделаем их. Это, опять же, довольно гибкие испытания того, что, учитывая минуту в процессе разработки программиста, он считает, что каждую конечную точку должно дать нам взамен на каждый вход.
В зависимости от сложности функции, которую мы работаем или на изменения, привлеченные владельцем продукта, мы можем полностью изменить их по пути.
Для этих сквозных испытаний, с целью использования примеров для многих случаев мы будем предполагать, что мы хотим, мы хотим, чтобы функциональные возможности Crud для обеих моделей, используя все шесть таких методов http, как это:
GET api/transactions List all transaction objects POST api/transactions Create a transaction object GET api/transactions Retrieve a transaction object PUT api/transactions/hash Update a transaction object PATCH api/transactions/hash Update a field of a transaction object DELETE api/transactions/hash Delete a transaction object
Для этого мы будем:
- Сделайте приспособление для Apiclient Obslient Object и назвать его
api_client
Чтобы начать тестирование непосредственно с конечной точки:
# conftest.py @pytest.fixture def api_client(): return APIClient
Хорошая идея для этого прибора, так как оно не только имеет отношение к этому приложению, – это определить его в Conftest.py
Файл выше всех приложений, чтобы его можно было бы поделиться среди всех:
.... ├── tests │ ├── __init__.py │ ├── conftest.py <-- │ ├── test_app1 │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── factories.py │ │ ├── e2e_tests.py │ │ ├── test_models.py │ │ ├── test_signals.py │ │ ├── test_serializers.py │ │ ├── test_utils.py │ │ ├── test_views.py │ │ └── test_urls.py │ │ │ └── ... └── ...
Теперь давайте перейдем к тестированию всех конечных точек:
from model_bakery import baker import factory import json import pytest from apps.transaction.models import Transaction, Currency pytestmark = pytest.mark.django_db class TestCurrencyEndpoints: endpoint = '/api/currencies/' def test_list(self, api_client): baker.make(Currency, _quantity=3) response = api_client().get( self.endpoint ) assert response.status_code == 200 assert len(json.loads(response.content)) == 3 def test_create(self, api_client): currency = baker.prepare(Currency) expected_json = { 'name': currency.name, 'code': currency.code, 'symbol': currency.symbol } response = api_client().post( self.endpoint, data=expected_json, format='json' ) assert response.status_code == 201 assert json.loads(response.content) == expected_json def test_retrieve(self, api_client): currency = baker.make(Currency) expected_json = { 'name': currency.name, 'code': currency.code, 'symbol': currency.symbol } url = f'{self.endpoint}{currency.id}/' response = api_client().get(url) assert response.status_code == 200 assert json.loads(response.content) == expected_json def test_update(self, rf, api_client): old_currency = baker.make(Currency) new_currency = baker.prepare(Currency) currency_dict = { 'code': new_currency.code, 'name': new_currency.name, 'symbol': new_currency.symbol } url = f'{self.endpoint}{old_currency.id}/' response = api_client().put( url, currency_dict, format='json' ) assert response.status_code == 200 assert json.loads(response.content) == currency_dict @pytest.mark.parametrize('field',[ ('code'), ('name'), ('symbol'), ]) def test_partial_update(self, mocker, rf, field, api_client): currency = baker.make(Currency) currency_dict = { 'code': currency.code, 'name': currency.name, 'symbol': currency.symbol } valid_field = currency_dict[field] url = f'{self.endpoint}{currency.id}/' response = api_client().patch( url, {field: valid_field}, format='json' ) assert response.status_code == 200 assert json.loads(response.content)[field] == valid_field def test_delete(self, mocker, api_client): currency = baker.make(Currency) url = f'{self.endpoint}{currency.id}/' response = api_client().delete(url) assert response.status_code == 204 assert Currency.objects.all().count() == 0 class TestTransactionEndpoints: endpoint = '/api/transactions/' def test_list(self, api_client, utbb): client = api_client() utbb(3) url = self.endpoint response = client.get(url) assert response.status_code == 200 assert len(json.loads(response.content)) == 3 def test_create(self, api_client, utbb): client = api_client() t = utbb(1)[0] valid_data_dict = { 'amount_in_cents': t.amount_in_cents, 'currency': t.currency.code, 'name': t.name, 'email': t.email, 'message': t.message } url = self.endpoint response = client.post( url, valid_data_dict, format='json' ) assert response.status_code == 201 assert json.loads(response.content) == valid_data_dict assert Transaction.objects.last().link def test_retrieve(self, api_client, ftb): t = ftb() t = Transaction.objects.last() expected_json = t.__dict__ expected_json['link'] = t.link expected_json['currency'] = t.currency.code expected_json['creation_date'] = expected_json['creation_date'].strftime( '%Y-%m-%dT%H:%M:%S.%fZ' ) expected_json.pop('_state') expected_json.pop('currency_id') url = f'{self.endpoint}{t.id}/' response = api_client().get(url) assert response.status_code == 200 or response.status_code == 301 assert json.loads(response.content) == expected_json def test_update(self, api_client, utbb): old_transaction = utbb(1)[0] t = utbb(1)[0] expected_json = t.__dict__ expected_json['id'] = old_transaction.id.hashid expected_json['currency'] = old_transaction.currency.code expected_json['link'] = Transaction.objects.first().link expected_json['creation_date'] = old_transaction.creation_date.strftime( '%Y-%m-%dT%H:%M:%S.%fZ' ) expected_json.pop('_state') expected_json.pop('currency_id') url = f'{self.endpoint}{old_transaction.id}/' response = api_client().put( url, data=expected_json, format='json' ) assert response.status_code == 200 or response.status_code == 301 assert json.loads(response.content) == expected_json @pytest.mark.parametrize('field',[ ('name'), ('billing_name'), ('billing_email'), ('email'), ('amount_in_cents'), ('message'), ]) def test_partial_update(self, api_client, field, utbb): utbb(2) old_transaction = Transaction.objects.first() new_transaction = Transaction.objects.last() valid_field = { field: new_transaction.__dict__[field], } url = f'{self.endpoint}{old_transaction.id}/' response = api_client().patch( path=url, data=valid_field, format='json', ) assert response.status_code == 200 or response.status_code == 301 try: assert json.loads(response.content)[field] == valid_field[field] except json.decoder.JSONDecodeError as e: pass def test_delete(self, api_client, utbb): transaction = utbb(1)[0] url = f'{self.endpoint}{transaction.id}/' response = api_client().delete( url ) assert response.status_code == 204 or response.status_code == 301
После того, как у нас есть тесты на предварительные ожидаемые результаты наших конечных точек для наших приложений, мы можем приступить к созданию остальной части приложения из моделей вверх.
Утилизация
Utils – это функции помощника, которые будут распространяться по всему нашему коду, чтобы вы могли бы создавать их, а их корреспондентские испытания в любом порядке.
Первый Util, который мы собираемся сделать, это fill_transaction
Функция, которая, учитывая экземпляр модели транзакции, заполнит поля, которые не предназначены для заполнения пользователем.
Одно поле мы можем заполнить на бэкэнда, это programe_intent_id
поле. «Намерение платежа» – это путь Полоса (платежный сервис) представляет собой операции, ожидаемые; И это удостоверение личности, чтобы поставить его в простые термины, так это то, как они могут найти данные об этом в их БД.
Таким образом, util, который использует библиотеку Python Stripe для создания и извлечения идентификатора намерения платежа, может быть это:
def fill_transaction(transaction): payment_intent_id = stripe.PaymentIntent.create( amount=amount, currency=currency.code.lower(), payment_method_types=['card'], ).id t = transaction.__class__.objects.filter(id=transaction.id) t.update( # We use update not to trigger a save-signal recursion Overflow payment_intent_id=payment_intent_id, )
Тест на этот UTIL должен издеваться над вызовом API и вызовы 2 дБ:
class TestFillTransaction: def test_function_code(self, mocker): t = FilledTransactionFactory.build() pi = PaymentIntentFactory() create_pi_mock = mocker.Mock(return_value=pi) stripe.PaymentIntent.create = create_pi_mock filter_call_mock = mocker.Mock() Transaction.objects.filter = filter_call_mock update_call_mock = mocker.Mock() filter_call_mock.return_value.update = update_call_mock utils.fill_transaction(t) filter_call_mock.assert_called_with(id=t.id) update_call_mock.assert_called_with( payment_intent_id=pi.id, stripe_response=pi.last_response.data, billing_email=t.email, billing_name=t.name, )
Сигналы
Для сигналов мы можем иметь сигнал для запуска нашего fill_transaction
Util, когда транзакция создана.
from django.db.models.signals import pre_save from django.dispatch import receiver from apps.transaction.models import Transaction from apps.transaction.utils import fill_transaction @receiver(pre_save, sender=Transaction) def transaction_filler(sender, instance, *args, **kwargs): """Fills fields""" if not instance.id: fill_transaction(instance)
Этот сигнал будет, кстати, неявно проверен на E2E. Хороший явный тест на этот сигнал может быть следующим:
import pytest from django.db.models.signals import pre_save from apps.transaction.models import Transaction from tests.test_transaction.factories import UnfilledTransactionFactory, FilledTransactionFactory pytestmark = pytest.mark.unit class TestTransactionFiller: def test_pre_save(self, mocker): instance = UnfilledTransactionFactory.build() mock = mocker.patch( 'apps.transaction.signals.fill_transaction' ) pre_save.send(Transaction, instance=instance, created=True) mock.assert_called_with(instance)
Сериализаторы
Для нашего приложения у нас будет один один сериал для нашей модели валюты и двух сериализаторов для нашей модели транзакции:
- Тот, который содержит поля, которые могут быть изменены менеджером транзакций (кто-то создает и удаление транзакций)
- Тот, который содержит поля, которые можно увидеть «менеджерами транзакций» или также теми, кто будет теми, кто платит.
from hashid_field.rest import HashidSerializerCharField from rest_framework import serializers from django.conf import settings from django.core.validators import MaxLengthValidator, ProhibitNullCharactersValidator from rest_framework.validators import ProhibitSurrogateCharactersValidator from apps.transaction.models import Currency, Transaction class CurrencySerializer(serializers.ModelSerializer): class Meta: model = Currency fields = ['name', 'code', 'symbol'] if settings.DEBUG == True: extra_kwargs = { 'name': { 'validators': [MaxLengthValidator, ProhibitNullCharactersValidator] }, 'code': { 'validators': [MaxLengthValidator, ProhibitNullCharactersValidator] } } class UnfilledTransactionSerializer(serializers.ModelSerializer): currency = serializers.SlugRelatedField( slug_field='code', queryset=Currency.objects.all(), ) class Meta: model = Transaction fields = ( 'name', 'currency', 'email', 'amount_in_cents', 'message' ) class FilledTransactionSerializer(serializers.ModelSerializer): id = HashidSerializerCharField(source_field='transaction.Transaction.id', read_only=True) currency = serializers.StringRelatedField(read_only=True) link = serializers.ReadOnlyField() class Meta: model = Transaction fields = '__all__' extra_kwargs = { """Non editable fields""" 'id': {'read_only': True}, 'creation_date': {'read_only': True}, 'payment_date': {'read_only': True}, 'amount_in_cents': {'read_only': True}, 'payment_intent_id': {'read_only': True}, 'payment_status': {'read_only': True}, }
Установка тестов для сериализаторов должна стремиться к тестированию двух вещей (когда актуальны):
- Что он может правильно сериализировать экземпляр модели
- Что он может правильно превратить действительные сериализованные данные в модель (a.k.a “десериализация”)
import pytest import factory from rest_framework.fields import CharField from apps.transaction.api.serializers import CurrencySerializer, UnfilledTransactionSerializer, FilledTransactionSerializer from tests.test_transaction.factories import CurrencyFactory, UnfilledTransactionFactory, FilledTransactionFactory class TestCurrencySerializer: @pytest.mark.unit def test_serialize_model(self): currency = CurrencyFactory.build() serializer = CurrencySerializer(currency) assert serializer.data @pytest.mark.unit def test_serialized_data(self, mocker): valid_serialized_data = factory.build( dict, FACTORY_CLASS=CurrencyFactory ) serializer = CurrencySerializer(data=valid_serialized_data) assert serializer.is_valid() assert serializer.errors == {} class TestUnfilledTransactionSerializer: @pytest.mark.unit def test_serialize_model(self): t = UnfilledTransactionFactory.build() expected_serialized_data = { 'name': t.name, 'currency': t.currency.code, 'email': t.email, 'amount_in_cents': t.amount_in_cents, 'message': t.message, } serializer = UnfilledTransactionSerializer(t) assert serializer.data == expected_serialized_data https://docs.pytest.org/en/stable/flaky.html @pytest.mark.django_db def test_serialized_data(self, mocker): c = CurrencyFactory() t = UnfilledTransactionFactory.build(currency=c) valid_serialized_data = { 'name': t.name, 'currency': t.currency.code, 'email': t.email, 'amount_in_cents': t.amount_in_cents, 'message': t.message, } serializer = UnfilledTransactionSerializer(data=valid_serialized_data) assert serializer.is_valid(raise_exception=True) assert serializer.errors == {} class TestFilledTransactionSerializer: @pytest.mark.unit def test_serialize_model(self, ftd): t = FilledTransactionFactory.build() expected_serialized_data = ftd(t) serializer = FilledTransactionSerializer(t) assert serializer.data == expected_serialized_data @pytest.mark.unit def test_serialized_data(self): t = FilledTransactionFactory.build() valid_serialized_data = { 'id': t.id.hashid, 'name': t.name, 'currency': t.currency.code, 'creation_date': t.creation_date.strftime('%Y-%m-%dT%H:%M:%SZ'), 'payment_date': t.payment_date.strftime('%Y-%m-%dT%H:%M:%SZ'), 'stripe_response': t.stripe_response, 'payment_intent_id': t.payment_intent_id, 'billing_name': t.billing_name, 'billing_email': t.billing_email, 'payment_status': t.payment_status, 'link': t.link, 'email': t.email, 'amount_in_cents': t.amount_in_cents, 'message': t.message, } serializer = FilledTransactionSerializer(data=valid_serialized_data) assert serializer.is_valid(raise_exception=True) assert serializer.errors == {}
Визрители
Мы будем использовать DRF-оформления для операций CRUD в наших моделях, пропуская необходимость проверки конфигурации URL:
# in urls.py route_lists = [ transaction_urls.route_list, ] router = routers.DefaultRouter() for route_list in route_lists: for route in route_list: router.register(route[0], route[1]) urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ] # in views.py from rest_framework.viewsets import ModelViewSet from apps.transaction.api.serializers import CurrencySerializer, UnfilledTransactionSerializer, FilledTransactionSerializer from apps.transaction.models import Currency, Transaction class CurrencyViewSet(ModelViewSet): queryset = Currency.objects.all() serializer_class = CurrencySerializer class TransactionViewset(ModelViewSet): """Transaction Viewset""" queryset = Transaction.objects.all() def get_serializer_class(self): if self.action == 'create': return UnfilledTransactionSerializer else: return FilledTransactionSerializer
С простыми классовыми взглядами или на основе функциональных видов мы начнем тестировать вид с самой точки зрения и изолировать вид от URLConf, который вызывает этот вид. Но поскольку мы используем маршрутизаторы для этого, мы начнем тестирование визниц с самой конечной точки с помощью клиента API.
import factory import json import pytest from django.urls import reverse from django_mock_queries.mocks import MockSet from rest_framework.relations import RelatedField, SlugRelatedField from apps.transaction.api.serializers import UnfilledTransactionSerializer, CurrencySerializer from apps.transaction.api.views import CurrencyViewSet, TransactionViewset from apps.transaction.models import Currency, Transaction from tests.test_transaction.factories import CurrencyFactory, FilledTransactionFactory, UnfilledTransactionFactory pytestmark = [pytest.mark.urls('config.urls'), pytest.mark.unit] class TestCurrencyViewset: def test_list(self, mocker, rf): # Arrange url = reverse('currency-list') request = rf.get(url) qs = MockSet( CurrencyFactory.build(), CurrencyFactory.build(), CurrencyFactory.build() ) view = CurrencyViewSet.as_view( {'get': 'list'} ) #Mcking mocker.patch.object( CurrencyViewSet, 'get_queryset', return_value=qs ) # Act response = view(request).render() #Assert assert response.status_code == 200 assert len(json.loads(response.content)) == 3 def test_retrieve(self, mocker, rf): currency = CurrencyFactory.build() expected_json = { 'name': currency.name, 'code': currency.code, 'symbol': currency.symbol } url = reverse('currency-detail', kwargs={'pk': currency.id}) request = rf.get(url) mocker.patch.object( CurrencyViewSet, 'get_queryset', return_value=MockSet(currency) ) view = CurrencyViewSet.as_view( {'get': 'retrieve'} ) response = view(request, pk=currency.id).render() assert response.status_code == 200 assert json.loads(response.content) == expected_json def test_create(self, mocker, rf): valid_data_dict = factory.build( dict, FACTORY_CLASS=CurrencyFactory ) url = reverse('currency-list') request = rf.post( url, content_type='application/json', data=json.dumps(valid_data_dict) ) mocker.patch.object( Currency, 'save' ) view = CurrencyViewSet.as_view( {'post': 'create'} ) response = view(request).render() assert response.status_code == 201 assert json.loads(response.content) == valid_data_dict def test_update(self, mocker, rf): old_currency = CurrencyFactory.build() new_currency = CurrencyFactory.build() currency_dict = { 'code': new_currency.code, 'name': new_currency.name, 'symbol': new_currency.symbol } url = reverse('currency-detail', kwargs={'pk': old_currency.id}) request = rf.put( url, content_type='application/json', data=json.dumps(currency_dict) ) mocker.patch.object( CurrencyViewSet, 'get_object', return_value=old_currency ) mocker.patch.object( Currency, 'save' ) view = CurrencyViewSet.as_view( {'put': 'update'} ) response = view(request, pk=old_currency.id).render() assert response.status_code == 200 assert json.loads(response.content) == currency_dict @pytest.mark.parametrize('field',[ ('code'), ('name'), ('symbol'), ]) def test_partial_update(self, mocker, rf, field): currency = CurrencyFactory.build() currency_dict = { 'code': currency.code, 'name': currency.name, 'symbol': currency.symbol } valid_field = currency_dict[field] url = reverse('currency-detail', kwargs={'pk': currency.id}) request = rf.patch( url, content_type='application/json', data=json.dumps({field: valid_field}) ) mocker.patch.object( CurrencyViewSet, 'get_object', return_value=currency ) mocker.patch.object( Currency, 'save' ) view = CurrencyViewSet.as_view( {'patch': 'partial_update'} ) response = view(request).render() assert response.status_code == 200 assert json.loads(response.content)[field] == valid_field def test_delete(self, mocker, rf): currency = CurrencyFactory.build() url = reverse('currency-detail', kwargs={'pk': currency.id}) request = rf.delete(url) mocker.patch.object( CurrencyViewSet, 'get_object', return_value=currency ) del_mock = mocker.patch.object( Currency, 'delete' ) view = CurrencyViewSet.as_view( {'delete': 'destroy'} ) response = view(request).render() assert response.status_code == 204 assert del_mock.assert_called class TestTransactionViewset: def test_list(self, mocker, rf): url = reverse('transaction-list') request = rf.get(url) qs = MockSet( FilledTransactionFactory.build(), FilledTransactionFactory.build(), FilledTransactionFactory.build() ) mocker.patch.object( TransactionViewset, 'get_queryset', return_value=qs ) view = TransactionViewset.as_view( {'get': 'list'} ) response = view(request).render() assert response.status_code == 200 assert len(json.loads(response.content)) == 3 def test_create(self, mocker, rf): valid_data_dict = factory.build( dict, FACTORY_CLASS=UnfilledTransactionFactory ) currency = valid_data_dict['currency'] valid_data_dict['currency'] = currency.code url = reverse('transaction-list') request = rf.post( url, content_type='application/json', data=json.dumps(valid_data_dict) ) retrieve_currency = mocker.Mock(return_value=currency) SlugRelatedField.to_internal_value = retrieve_currency mocker.patch.object( Transaction, 'save' ) view = TransactionViewset.as_view( {'post': 'create'} ) response = view(request).render() assert response.status_code == 201 assert json.loads(response.content) == valid_data_dict def test_retrieve(self, api_client, mocker, ftd): transaction = FilledTransactionFactory.build() expected_json = ftd(transaction) url = reverse( 'transaction-detail', kwargs={'pk': transaction.id} ) TransactionViewset.get_queryset = mocker.Mock( return_value=MockSet(transaction) ) response = api_client().get(url) assert response.status_code == 200 assert json.loads(response.content) == expected_json def test_update(self, mocker, api_client, ftd): old_transaction = FilledTransactionFactory.build() new_transaction = FilledTransactionFactory.build() transaction_json = ftd(new_transaction, old_transaction) url = reverse( 'transaction-detail', kwargs={'pk': old_transaction.id} ) retrieve_currency = mocker.Mock( return_value=old_transaction.currency ) SlugRelatedField.to_internal_value = retrieve_currency mocker.patch.object( TransactionViewset, 'get_object', return_value=old_transaction ) Transaction.save = mocker.Mock() response = api_client().put( url, data=transaction_json, format='json' ) assert response.status_code == 200 assert json.loads(response.content) == transaction_json @pytest.mark.parametrize('field',[ ('name'), ('billing_name'), ('billing_email'), ('email'), ('amount_in_cents'), ('message'), ]) def test_partial_update(self, mocker, api_client, field): old_transaction = FilledTransactionFactory.build() new_transaction = FilledTransactionFactory.build() valid_field = { field: new_transaction.__dict__[field] } url = reverse( 'transaction-detail', kwargs={'pk': old_transaction.id} ) SlugRelatedField.to_internal_value = mocker.Mock( return_value=old_transaction.currency ) mocker.patch.object( TransactionViewset, 'get_object', return_value=old_transaction ) Transaction.save = mocker.Mock() response = api_client().patch( url, data=valid_field, format='json' ) assert response.status_code == 200 assert json.loads(response.content)[field] == valid_field[field] def test_delete(self, mocker, api_client): transaction = FilledTransactionFactory.build() url = reverse('transaction-detail', kwargs={'pk': transaction.id}) mocker.patch.object( TransactionViewset, 'get_object', return_value=transaction ) del_mock = mocker.patch.object( Transaction, 'delete' ) response = api_client().delete( url ) assert response.status_code == 204 assert del_mock.assert_called
Учитывая, что у нас будет код с разными логическими последствиями, мы можем проверить, сколько из кода мы покрываем нашими тестами (ака «покрытие» нашего тестового набора) в процентных терминах, используя pytest-cov.
плагин. Для того, чтобы увидеть охват наших тестов, нам придется использовать команду –cov.
Охват не скажет вам, если ваш код сломается или нет, он только скажет вам, сколько он у вас покрыл тестами, и это не имеет никакого отношения к тому, что вы сделали актуальные тесты или нет.
Это хорошая идея вручную изменять наши настройки покрытия внутри нашего setup.cfg
под [Охват: беги]
(или, если вы хотите иметь автономный файл, внутри .ceveragerc
Файл под [RUN]) и установите, какие каталоги мы хотели проверить на охват, и какие файлы мы можем захотеть исключить внутри тех. У меня сам все мои приложения внутри приложения
каталог, так мой setup.cfg
будет выглядеть что-то подобное:
[tool:pytest] ... [coverage:run] source=apps omit=*/migrations/*,
Если мы запустим pytest --cov.cfg
(Вы могли бы включить его в свои дополнения), у нас может быть выход: Что у меня 100% ничего не значит. Опять же, если вы получите 90%, потому что вы оставили неактуальный кусок непроверенного кода, ваш охват будет настолько хорошим на 100%.
Отказ от ответственности : Pytest-Cov
Сообщается, что плагин несовместим с отладчиком VSCode, поэтому вы можете удалить его команду с дополнительными вершинами или иногда даже удалить его из вашего проекта.
Флакистые тесты вызваны должным образом изолирующими тесты между собой. Это должно быть приемлемо при запуске E2E тестов, но это должно быть красное предупреждение при проведении с модульными тестами.
- Простые просмотры
- Испытания конфигурации URL
- Список ресурсов
- Импорт очистки
Оригинал: “https://dev.to/sherlockcodes/pytest-with-django-rest-framework-from-zero-to-hero-8c4”