Рубрики
Без рубрики

Pteest с Django Read Framework: Из грязи в князи

Для кого эта статья? Для разработчиков, которые хотят узнать все, что им нужно стать … Помечено Django, Python, тестирование, Testde.

Для разработчиков, которые хотят узнать все, что им нужно, чтобы стать эффективными и эффективными тестерами, у которых нет команды 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 (Тестовое развитие): создание тестов, прежде чем утверждать их через код.
  • Тестирование после разработки : Тестирование куска кода сразу после его создания.

Если мы не будем очень ознакомиться с синтаксисом и кодом, мы ожидаем написать (почти невозможно, когда мы еще не уверены в коде, мы будем писать), это почти всегда хорошая идея, чтобы пойти с последним. То есть, чтобы написать наш код и после того, как мы рассмотрим его закончить, перейдите к тестируемую, как ожидалось. Но я рекомендую их сочетание:

  1. Сделайте сквозные тесты : Это должен быть компонент TDD нашего конечного тестового номера. Эти тесты помогут вам в первую очередь и прежде всего для макета.
  2. Создание модульных тестов : Напишите код и создайте изолированные тесты для каждой из частей приложения 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». То есть тесты должны следовать этому порядку:

  1. Организовать : Установите все необходимое для теста
  2. Макет : издеваться, что нужно было изолировать ваш тест
  3. Акт : Запустите свой код код.
  4. Утверждать : утверждать, что результат точно так же, как и ожидалось, чтобы избежать каких-либо неприятных сюрпризов позже.

Поэтому тестовая структура должна выглядеть так:

# 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  

Для этого мы будем:

  1. Сделайте приспособление для 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”