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

Тестовая разработка с помощью pytest

Автор оригинала: Marcus Sanatan.

Вступление

Хорошее программное обеспечение-это проверенное программное обеспечение. Тестирование нашего кода может помочь нам поймать ошибки или нежелательное поведение.

Test Driven Development (TDD) – это практика разработки программного обеспечения, которая требует от нас постепенного написания тестов для функций, которые мы хотим добавить. Он использует автоматизированные наборы тестирования, такие как pytest – тестовая платформа для программ на Python.

  • Автоматизированное Тестирование
  • Модуль pytest
  • Что такое Тест-ориентированная разработка?
  • Кто использует TDD для создания приложений?
  • Покрытие кода
  • Модульный тест против Интеграционных тестов
  • Основной пример: Вычисление суммы простых чисел
  • Расширенный пример: Написание менеджера инвентаризации
  • Вывод

Автоматизированное Тестирование

Разработчики обычно пишут код, компилируют его, если это необходимо, а затем запускают код, чтобы увидеть, работает ли он. Это пример ручного тестирования . В этом методе мы исследуем, какие особенности работы программы. Если вы хотите провести тщательное тестирование, вы должны помнить, как проверить различные результаты каждой функции.

Что, если новый разработчик начнет добавлять функции в проект, вам придется изучить их функции, чтобы протестировать его? Новые функции иногда влияют на старые функции, собираетесь ли вы вручную проверить, что все предыдущие функции все еще функционируют, когда вы добавили новую?

Ручное тестирование может дать нам быстрый импульс уверенности в продолжении разработки. Однако по мере роста нашего приложения становится все труднее и утомительнее непрерывно тестировать нашу кодовую базу вручную.

Автоматизированное тестирование перекладывает бремя тестирования кода на нас самих и отслеживания результатов на поддержание скриптов, которые делают это за нас. Скрипты запускают модули кода с входными данными, определенными разработчиком, и сравнивают выходные данные с ожиданиями, определенными разработчиком.

Модуль pytest

Стандартная библиотека Python поставляется с автоматизированной платформой тестирования – библиотекой unittest . В то время как библиотека unittest является многофункциональной и эффективной в своей задаче, мы будем использовать pytest в качестве нашего оружия выбора в этой статье.

Большинство разработчиков находят pytest более простым в использовании, чем unittest . Одна простая причина заключается в том, что pytest требует только функций для написания тестов, в то время как модуль unittest требует классов.

Для многих новых разработчиков требование классов для тестов может быть немного отталкивающим. pytest также включает в себя множество других функций, которые мы будем использовать позже в этом уроке и которые отсутствуют в модуле unittest .

Что такое Тест-ориентированная разработка?

Тестовая разработка-это простая практика разработки программного обеспечения, которая инструктирует вас или команду программистов следовать этим древовидным шагам для создания программного обеспечения:

  1. Напишите тест для функции, которая терпит неудачу
  2. Напишите код, чтобы сделать тест пройденным
  3. Рефакторинг кода по мере необходимости

Этот процесс обычно называют циклом Red-Green-Refactor :

  • Вы пишете автоматический тест на то, как должен вести себя новый код, и видите, что он терпит неудачу – Red
  • Пишите код в приложении до тех пор, пока ваш тест не пройдет – Зеленый
  • Рефакторинг код, чтобы сделать его читабельным и эффективным. Вам не нужно беспокоиться о том, что ваш рефакторинг сломает новую функцию, вам просто нужно повторно запустить тест и убедиться, что он пройдет.

Функция завершается, когда нам больше не нужно писать код для прохождения ее тестов.

Кто использует TDD для создания приложений?

Распространенная жалоба на использование TDD заключается в том, что это занимает слишком много времени.

По мере того как вы становитесь более эффективными с написанием тестов, время, необходимое вам для их поддержания, уменьшается. Кроме того, TDD предоставляет следующие преимущества, которые вы можете найти стоящим компромисса во времени:

  • Написание тестов требует, чтобы вы знали входы и выходы, чтобы заставить функцию работать – TDD заставляет нас думать об интерфейсе приложения, прежде чем мы начнем кодировать.
  • Повышенная уверенность в кодовой базе – Имея автоматизированные тесты для всех функций, разработчики чувствуют себя более уверенно при разработке новых функций. Становится тривиальным проверить всю систему, чтобы увидеть, не нарушили ли новые изменения то, что существовало раньше.
  • TDD не устраняет все ошибки, но вероятность столкнуться с ними ниже – При попытке исправить ошибку вы можете написать для нее тест, чтобы убедиться, что она исправлена после завершения кодирования.
  • Тесты могут быть использованы в качестве дополнительной документации. Когда мы пишем входы и выходы функции, разработчик может посмотреть на тест и увидеть, как должен использоваться интерфейс кода.

Покрытие кода

Покрытие кода – это метрика, которая измеряет объем исходного кода, охватываемого вашим планом тестирования.

100% покрытие кода означает, что весь написанный вами код был использован некоторыми тестами. Инструменты измеряют покрытие кода многими различными способами, вот несколько популярных метрик:

  • Проверенные строки кода
  • Сколько определенных функций тестируется
  • Сколько ветвей (например, операторов if ) тестируется

Важно, чтобы вы знали, какие метрики используются вашим инструментом покрытия кода.

Поскольку мы активно используем pytest , мы будем использовать популярный плагин pytest-cov для получения покрытия кода.

Высокий охват кода не означает, что в вашем приложении не будет ошибок. Более чем вероятно, что код не был протестирован для всех возможных сценариев.

Модульный тест против Интеграционных тестов

Модульные тесты используются для обеспечения того, чтобы отдельный модуль вел себя так, как ожидалось, в то время как интеграционные тесты гарантируют, что набор модулей взаимодействует так, как мы их ожидаем.

По мере разработки более крупных приложений нам придется разрабатывать множество компонентов. Хотя каждый из этих отдельных компонентов может иметь свои соответствующие модульные тесты , мы также хотим, чтобы эти несколько компонентов при совместном использовании соответствовали нашим ожиданиям.

TDD требует, чтобы мы начали с написания одного теста, который не работает с текущей базой кода, а затем работали над его завершением. Он не указывает, что это должен быть модульный тест, ваш первый тест может быть интеграционным тестом, если вы хотите.

Когда ваш первый провальный интеграционный тест будет написан, мы сможем приступить к разработке каждого отдельного компонента.

Интеграционный тест будет провален до тех пор, пока каждый компонент не будет построен и не пройдет свои тесты. Когда интеграционный тест пройдет, если он будет создан правильно, мы будем соответствовать требованиям пользователя к нашей системе.

Основной пример: Вычисление суммы простых чисел

Лучший способ понять TDD-это применить его на практике. Мы начнем с написания программы на Python, которая возвращает сумму всех чисел в последовательности, которые являются простыми числами.

Для этого мы создадим две функции: одну, которая определяет, является ли число простым или нет, и другую, которая добавляет простые числа из заданной последовательности чисел.

Создайте каталог с именем primes в выбранном вами рабочем пространстве. Теперь добавьте два файла: primes.py , test_primes.py . В первом файле мы напишем ваш программный код, во втором-наши тесты.

pytest требует, чтобы наши тестовые файлы либо начинались с “test_”, либо заканчивались “_test.py” (поэтому мы могли бы также назвать наш тестовый файл primes_test.py ).

Теперь в нашем каталоге primes давайте настроим нашу виртуальную среду:

$ python3 -m venv env # Create a virtual environment for our modules
$ . env/bin/activate # Activate our virtual environment
$ pip install --upgrade pip # Upgrade pip
$ pip install pytest # Install pytest

Тестирование функции is_prime()

Простое число-это любое натуральное число больше 1, которое делится только на 1 и само по себе.

Наша функция должна принимать число и возвращать True , если оно простое, и False в противном случае.

В нашем test_primes.py , давайте добавим наш первый тестовый случай:

def test_prime_low_number():
    assert is_prime(1) == False

Оператор assert () – это ключевое слово в Python (и во многих других языках), которое немедленно выдает ошибку, если условие не выполняется. Это ключевое слово полезно при написании тестов, потому что оно указывает именно на то, какое условие не удалось.

Если мы войдем 1 или число меньшее, чем 1 – тогда он не может быть простым.

Давайте теперь проведем наш тест. Введите в командной строке следующее:

$ pytest

Для подробного вывода вы можете запустить pytest -v . Убедитесь, что ваша виртуальная среда все еще активна (вы должны увидеть (env) в начале строки в вашем терминале).

Вы должны заметить такой вывод:

    def test_prime_low_number():
>       assert is_prime(1) == False
E       NameError: name 'is_prime' is not defined

test_primes.py:2: NameError
========================================================= 1 failed in 0.12 seconds =========================================================

Имеет смысл получить NameError , мы еще не создали нашу функцию. Это “красный” аспект цикла красный-зеленый-рефактор.

pytest даже регистрирует неудачные тесты в красном цвете, если ваша оболочка настроена на отображение цветов. Теперь давайте добавим код в наш primes.py файл для прохождения этого теста:

def is_prime(num):
    if num == 1:
        return False

Примечание : Обычно рекомендуется хранить тесты в отдельных файлах от кода. Помимо улучшения читабельности и разделения проблем по мере роста вашей кодовой базы, это также удерживает разработчика теста вдали от внутренней работы кода. Поэтому тесты используют интерфейсы приложений точно так же, как это сделал бы другой разработчик.

Теперь давайте еще раз запустим pytest . Теперь мы должны увидеть такой результат:

=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/stackabuse/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 1 item

test_primes.py .                                                                                                                     [100%]

========================================================= 1 passed in 0.04 seconds =========================================================

Наше первое испытание прошло! Мы знаем, что 1 не является простым, но по определению 0 не является простым, как и любое отрицательное число.

Мы должны рефакторинговать наше приложение, чтобы отразить это, и изменить is_prime() на:

def is_prime(num):
    # Prime numbers must be greater than 1
    if num < 2:
        return False

Если мы снова запустим pytest , наши тесты все равно пройдут.

Теперь давайте добавим тестовый случай для простого числа, в test_primes.py добавьте следующее после нашего первого тестового случая:

def test_prime_prime_number():
    assert is_prime(29)

И давайте запустим pytest , чтобы увидеть этот вывод:

    def test_prime_prime_number():
>       assert is_prime(29)
E       assert None
E        +  where None = is_prime(29)

test_primes.py:9: AssertionError
============================================================= warnings summary =============================================================
test_primes.py::test_prime_prime_number
  /Users/marcus/stackabuse/test-driven-development-with-pytest/primes/test_primes.py:9: PytestWarning: asserting the value None, please use "assert is None"
    assert is_prime(29)

-- Docs: https://docs.pytest.org/en/latest/warnings.html
============================================== 1 failed, 1 passed, 1 warnings in 0.12 seconds ==============================================

Обратите внимание, что команда pytest теперь запускает два теста, которые мы написали.

Новый случай терпит неудачу, поскольку мы на самом деле не вычисляем, является ли число простым или нет. Функция is_prime() возвращает None , как и другие функции по умолчанию для любого числа больше 1.

Выход по-прежнему не работает, или мы видим красный цвет на выходе.

Давайте подумаем о том, как мы определяем, является ли число простым или нет. Простейшим методом будет цикл от 2 до одного меньше числа, деля число на текущее значение итерации.

Чтобы сделать это более эффективным, мы можем проверить, разделив числа между 2 и квадратным корнем из числа.

Если нет остатка от деления, то у него есть делитель, который не является ни 1, ни самим собой и, следовательно, не является простым. Если он не находит делителя в цикле, то он должен быть простым.

Давайте обновим is_prime() с нашей новой логикой:

import math

def is_prime(num):
    # Prime numbers must be greater than 1
    if num < 2:
        return False
    for n in range(2, math.floor(math.sqrt(num) + 1)):
        if num % n == 0:
            return False
    return True

Теперь мы запускаем pytest , чтобы увидеть, проходит ли наш тест:

=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/stackabuse/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 2 items

test_primes.py ..                                                                                                                    [100%]

========================================================= 2 passed in 0.04 seconds =========================================================

Это проходит. Мы знаем, что эта функция может получить как простое число, так и малое. Давайте добавим тест, чтобы убедиться, что он возвращает False для составного числа больше 1.

В test_primes.py добавьте следующий тестовый пример ниже:

def test_prime_composite_number():
    assert is_prime(15) == False

Если мы запустим pytest , то увидим следующий вывод:

=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/stackabuse/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 3 items

test_primes.py ...                                                                                                                   [100%]

========================================================= 3 passed in 0.04 seconds =========================================================

Тестирование sum_of_primes()

Как и в случае с is_prime() , давайте подумаем о результатах этой функции. Если функции задан пустой список, то сумма должна быть равна нулю.

Это гарантирует, что наша функция всегда должна возвращать значение с допустимыми входными данными. После этого мы хотим проверить, что он добавляет только простые числа в список чисел.

Давайте напишем наш первый неудачный тест, добавим следующий код в конце test_primes.py :

def test_sum_of_primes_empty_list():
    assert sum_of_primes([]) == 0

Если мы запустим pytest , то получим знакомую ошибку Name test failure, так как мы еще не определили функцию. В нашем primes.py файл давайте добавим нашу новую функцию, которая просто возвращает сумму заданного списка:

def sum_of_primes(nums):
    return sum(nums)

Теперь запуск pytest покажет, что все тесты пройдены. Наш следующий тест должен гарантировать, что будут добавлены только простые числа.

Мы будем смешивать простые и составные числа и ожидать, что функция будет добавлять только простые числа:

def test_sum_of_primes_mixed_list():
    assert sum_of_primes([11, 15, 17, 18, 20, 100]) == 28

Простые числа в списке, который мы тестируем, – это 11 и 17, которые складываются в 28.

Запуск pytest для проверки того, что новый тест не прошел. Теперь давайте изменим наш sum_of_primes() так, чтобы добавлялись только простые числа.

Мы отфильтруем простые числа с помощью Понимания списка:

def sum_of_primes(nums):
    return sum([x for x in nums if is_prime(x)])

Как обычно, мы запускаем pytest , чтобы убедиться, что мы исправили неудачный тест – все проходит.

После завершения давайте проверим наше покрытие кода:

$ pytest --cov=primes

Для этого пакета наше покрытие кода составляет 100%! Если это не так, мы можем потратить некоторое время на добавление еще нескольких тестов в наш код, чтобы убедиться, что наш план тестирования является тщательным.

Например, если бы нашей функции is_prime() было присвоено значение float, выдала бы она ошибку? Наш метод is_prime() не применяет правило, что простое число должно быть натуральным числом, он только проверяет, что оно больше 1.

Несмотря на то, что у нас есть полный охват кода, реализуемая функция может работать неправильно не во всех ситуациях.

Расширенный пример: Написание менеджера инвентаризации

Теперь, когда мы постигли основы TDD, давайте глубже погрузимся в некоторые полезные функции pytest , которые позволяют нам стать более эффективными при написании тестов.

Как и раньше в нашем базовом примере, inventory.py , и тестовый файл, test_inventory.py , будут нашими основными двумя файлами.

Как и раньше в нашем базовом примере, || inventory.py || , и тестовый файл, || test_inventory.py || , будут нашими основными двумя файлами.

Магазин одежды и обуви хотел бы перенести управление своими товарами с бумаги на новый компьютер, который купил владелец. В то время как владелец хотел бы иметь много функций, он довольствуется программным обеспечением, которое может немедленно выполнять следующие предстоящие задачи.

  • Запишите 10 новых кроссовок Nike, которые она недавно купила. Каждый стоит $50,00.
  • Добавьте еще 5 спортивных штанов Adidas, которые стоят 70,00 долларов каждый.
  • Она ожидает, что клиент купит 2 кроссовки Nike
  • Она ожидает, что еще один клиент купит 1 из спортивных штанов.

Мы можем использовать эти требования для создания нашего первого интеграционного теста. Прежде чем мы приступим к его написанию, давайте немного конкретизируем более мелкие компоненты, чтобы выяснить, какими будут наши входы и выходы, сигнатуры функций и другие элементы дизайна системы.

Каждый товар будет иметь название, цену и количество. Мы сможем добавлять новые товары, добавлять запасы к существующим товарам и, конечно же, удалять запасы.

Когда мы создаем экземпляр объекта Inventory , мы хотим, чтобы пользователь предоставил limit . Значение limit по умолчанию будет равно 100. Наш первый тест состоял бы в том, чтобы проверить limit при создании экземпляра объекта. Чтобы убедиться, что мы не превысим наш лимит, нам нужно будет следить за счетчиком total_items . При инициализации это должно быть 0.

Нам нужно будет добавить в систему 10 кроссовок Nike и 5 спортивных штанов Adidas. Мы можем создать метод add_new_stock() , который принимает имя , цену и количество .

Мы должны проверить, можем ли мы добавить элемент в наш объект инвентаризации. Мы не должны иметь возможности добавить товар с отрицательным количеством, метод должен вызвать исключение. Мы также не должны иметь возможности добавлять больше элементов, если мы находимся на часовом пределе, что также должно вызвать исключение.

Клиенты будут покупать эти товары вскоре после входа, поэтому нам также понадобится метод remove_stock () . Эта функция будет нуждаться в названии запаса и количестве удаляемых товаров. Если удаляемое количество отрицательно или если оно делает общее количество для запаса ниже 0, то метод должен вызвать исключение. Кроме того, если предоставленное имя не найдено в нашем инвентаре, метод должен вызвать исключение.

Первые испытания

Подготовка к первым тестам помогла нам разработать нашу систему. Давайте начнем с создания нашего первого интеграционного теста:

def test_buy_and_sell_nikes_adidas():
    # Create inventory object
    inventory = Inventory()
    assert inventory.limit == 100
    assert inventory.total_items == 0

    # Add the new Nike sneakers
    inventory.add_new_stock('Nike Sneakers', 50.00, 10)
    assert inventory.total_items == 10

    # Add the new Adidas sweatpants
    inventory.add_new_stock('Adidas Sweatpants', 70.00, 5)
    assert inventory.total_items == 15

    # Remove 2 sneakers to sell to the first customer
    inventory.remove_stock('Nike Sneakers', 2)
    assert inventory.total_items == 13

    # Remove 1 sweatpants to sell to the next customer
    inventory.remove_stock('Adidas Sweatpants', 1)
    assert inventory.total_items == 12

При каждом действии мы делаем утверждение о состоянии инвентаря. Лучше всего утверждать после выполнения действия, чтобы при отладке вы знали, какой последний шаг был сделан.

Запустите pytest , и он должен завершиться с ошибкой NameError , так как класс Inventory не определен.

Давайте создадим наш Inventory class, с предельным параметром, который по умолчанию равен 100, начиная с модульных тестов:

def test_default_inventory():
    """Test that the default limit is 100"""
    inventory = Inventory()
    assert inventory.limit == 100
    assert inventory.total_items == 0

А теперь и сам класс:

class Inventory:
    def __init__(self, limit=100):
        self.limit = limit
        self.total_items = 0

Прежде чем мы перейдем к методам, мы хотим быть уверены, что наш объект может быть инициализирован с пользовательским ограничением, и он должен быть установлен правильно:

def test_custom_inventory_limit():
    """Test that we can set a custom limit"""
    inventory = Inventory(limit=25)
    assert inventory.limit == 25
    assert inventory.total_items == 0

Интеграция продолжает терпеть неудачу, но этот тест проходит.

Арматура

Наши первые два теста требовали, чтобы мы создали экземпляр объекта Inventory , прежде чем мы могли начать. Скорее всего, нам придется сделать то же самое для всех будущих тестов. Это немного однообразно.

Мы можем использовать светильники , чтобы помочь решить эту проблему. Приспособление-это известное и фиксированное состояние, с которым проводятся тесты, чтобы гарантировать повторяемость результатов.

Это хорошая практика, когда тесты выполняются изолированно друг от друга. Результаты одного теста не должны влиять на результаты другого теста.

Давайте создадим наше первое приспособление, объект Inventory без запасов.

test_inventory.py :

import pytest

@pytest.fixture
def no_stock_inventory():
    """Returns an empty inventory that can store 10 items"""
    return Inventory(10)

Обратите внимание на использование pytest.fixture | decorator . Для целей тестирования мы можем уменьшить лимит запасов до 10.

Давайте используем это приспособление для добавления теста для метода add_new_stock() :

def test_add_new_stock_success(no_stock_inventory):
    no_stock_inventory.add_new_stock('Test Jacket', 10.00, 5)
    assert no_stock_inventory.total_items == 5
    assert no_stock_inventory.stocks['Test Jacket']['price'] == 10.00
    assert no_stock_inventory.stocks['Test Jacket']['quantity'] == 5

Обратите внимание, что имя функции является аргументом теста, они должны иметь одно и то же имя для применяемого приспособления. В противном случае вы бы использовали его как обычный объект.

Чтобы убедиться, что запас был добавлен, мы должны протестировать немного больше, чем общее количество товаров, хранящихся до сих пор. Написание этого теста заставило нас задуматься о том, как мы показываем цену акции и оставшееся количество.

Запустите pytest , чтобы заметить, что теперь есть 2 сбоя и 2 прохода. Теперь мы добавим метод add_new_stock() :

class Inventory:
    def __init__(self, limit=100):
        self.limit = limit
        self.total_items = 0
        self.stocks = {}

    def add_new_stock(self, name, price, quantity):
        self.stocks[name] = {
            'price': price,
            'quantity': quantity
        }
        self.total_items += quantity

Вы заметите, что объект stocks был инициализирован в функции __init__ . Снова запустите pytest , чтобы подтвердить, что тест прошел.

Параметризационные тесты

Ранее мы упоминали, что метод add_new_stock() выполняет проверку входных данных – мы вызываем исключение, если количество равно нулю или отрицательно, или если оно переносит нас за пределы наших запасов.

Мы можем легко добавить больше тестовых случаев, используя try/except для перехвата каждого исключения. Это также кажется повторяющимся.

Pytest предоставляет параметризованные функции , которые позволяют нам тестировать несколько сценариев с использованием одной функции. Давайте напишем параметризованную тестовую функцию, чтобы убедиться, что наша проверка входных данных работает:

@pytest.mark.parametrize('name,price,quantity,exception', [
    ('Test Jacket', 10.00, 0, InvalidQuantityException(
        'Cannot add a quantity of 0. All new stocks must have at least 1 item'))
])
def test_add_new_stock_bad_input(name, price, quantity, exception):
    inventory = Inventory(10)
    try:
        inventory.add_new_stock(name, price, quantity)
    except InvalidQuantityException as inst:
        # First ensure the exception is of the right type
        assert isinstance(inst, type(exception))
        # Ensure that exceptions have the same message
        assert inst.args == exception.args
    else:
        pytest.fail("Expected error but found none")

Этот тест пытается добавить запас, получает исключение и затем проверяет, что это правильное исключение. Если мы не получим исключения, провалите тест. Предложение else очень важно в этом сценарии. Без него исключение, которое не было брошено, будет считаться пропуском. Таким образом, наш тест дал бы ложноположительный результат.

Мы используем pytest декораторы для добавления параметра в функцию. Первый аргумент содержит строку всех имен параметров. Второй аргумент – это список кортежей, где каждый кортеж является тестовым случаем.

Запустите pytest , чтобы увидеть, что наш тест не удался, так как InvalidQuantityException не определен. Назад в inventory.py давайте создадим новое исключение над классом Inventory :

class InvalidQuantityException(Exception):
    pass

И измените метод add_new_stock() :

def add_new_stock(self, name, price, quantity):
        if quantity <= 0:
            raise InvalidQuantityException(
                'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
        self.stocks[name] = {
            'price': price,
            'quantity': quantity
        }
        self.total_items += quantity

Запустите pytest , чтобы увидеть, что наш последний тест теперь проходит. Теперь давайте добавим второй тестовый случай ошибки, исключение возникает, если наш инвентарь не может его сохранить. Измените тест следующим образом:

@pytest.mark.parametrize('name,price,quantity,exception', [
    ('Test Jacket', 10.00, 0, InvalidQuantityException(
        'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
    ('Test Jacket', 10.00, 25, NoSpaceException(
        'Cannot add these 25 items. Only 10 more items can be stored'))
])
def test_add_new_stock_bad_input(name, price, quantity, exception):
    inventory = Inventory(10)
    try:
        inventory.add_new_stock(name, price, quantity)
    except (InvalidQuantityException, NoSpaceException) as inst:
        # First ensure the exception is of the right type
        assert isinstance(inst, type(exception))
        # Ensure that exceptions have the same message
        assert inst.args == exception.args
    else:
        pytest.fail("Expected error but found none")

Вместо того, чтобы создавать совершенно новую функцию, мы немного модифицируем ее, чтобы забрать наше новое исключение и добавить еще один кортеж в декоратор! Теперь два теста выполняются на одной функции.

Параметризованные функции сокращают время, необходимое для добавления новых тестовых случаев.

В inventory.py , сначала мы добавим наше новое исключение ниже InvalidQuantityException :

class NoSpaceException(Exception):
    pass

И измените метод add_new_stock() :

def add_new_stock(self, name, price, quantity):
    if quantity <= 0:
        raise InvalidQuantityException(
            'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
    if self.total_items + quantity > self.limit:
        remaining_space = self.limit - self.total_items
        raise NoSpaceException(
            'Cannot add these {} items. Only {} more items can be stored'.format(quantity, remaining_space))
    self.stocks[name] = {
        'price': price,
        'quantity': quantity
    }
    self.total_items += quantity

Запустите pytest , чтобы убедиться, что ваш новый тестовый случай также проходит.

Мы можем использовать светильники с нашей параметризованной функцией. Давайте рефакторим наш тест, чтобы использовать пустой инвентарь.:

def test_add_new_stock_bad_input(no_stock_inventory, name, price, quantity, exception):
    try:
        no_stock_inventory.add_new_stock(name, price, quantity)
    except (InvalidQuantityException, NoSpaceException) as inst:
        # First ensure the exception is of the right type
        assert isinstance(inst, type(exception))
        # Ensure that exceptions have the same message
        assert inst.args == exception.args
    else:
        pytest.fail("Expected error but found none")

Как и раньше, это просто еще один аргумент, который использует имя функции. Главное – исключить его в параметризованном декораторе.

Глядя на код еще немного, нет никакой причины, почему должно быть два метода для добавления новых акций. Мы можем проверить ошибки и успех в одной функции.

Удалите test_add_new_stock_bad_input() и test_add_new_stock_success() и давайте добавим новую функцию:

@pytest.mark.parametrize('name,price,quantity,exception', [
    ('Test Jacket', 10.00, 0, InvalidQuantityException(
        'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
    ('Test Jacket', 10.00, 25, NoSpaceException(
        'Cannot add these 25 items. Only 10 more items can be stored')),
    ('Test Jacket', 10.00, 5, None)
])
def test_add_new_stock(no_stock_inventory, name, price, quantity, exception):
    try:
        no_stock_inventory.add_new_stock(name, price, quantity)
    except (InvalidQuantityException, NoSpaceException) as inst:
        # First ensure the exception is of the right type
        assert isinstance(inst, type(exception))
        # Ensure that exceptions have the same message
        assert inst.args == exception.args
    else:
        assert no_stock_inventory.total_items == quantity
        assert no_stock_inventory.stocks[name]['price'] == price
        assert no_stock_inventory.stocks[name]['quantity'] == quantity

Эта одна тестовая функция сначала проверяет наличие известных исключений, если они не найдены, то мы гарантируем, что добавление соответствует нашим ожиданиям. Отдельная функция test_add_new_stock_success() теперь просто выполняется через параметр кортежа. Поскольку мы не ожидаем, что исключение будет выброшено в успешном случае, мы указываем None в качестве нашего исключения.

Завершение работы нашего менеджера по запасам

С нашим более продвинутым использованием pytest мы можем быстро разработать функцию remove_stock с помощью TDD. В inventory_test.py :

# The import statement needs one more exception
from inventory import Inventory, InvalidQuantityException, NoSpaceException, ItemNotFoundException

# ...
# Add a new fixture that contains stocks by default
# This makes writing tests easier for our remove function
@pytest.fixture
def ten_stock_inventory():
    """Returns an inventory with some test stock items"""
    inventory = Inventory(20)
    inventory.add_new_stock('Puma Test', 100.00, 8)
    inventory.add_new_stock('Reebok Test', 25.50, 2)
    return inventory

# ...
# Note the extra parameters, we need to set our expectation of
# what totals should be after our remove action
@pytest.mark.parametrize('name,quantity,exception,new_quantity,new_total', [
    ('Puma Test', 0,
     InvalidQuantityException(
         'Cannot remove a quantity of 0. Must remove at least 1 item'),
        0, 0),
    ('Not Here', 5,
     ItemNotFoundException(
         'Could not find Not Here in our stocks. Cannot remove non-existing stock'),
        0, 0),
    ('Puma Test', 25,
     InvalidQuantityException(
         'Cannot remove these 25 items. Only 8 items are in stock'),
     0, 0),
    ('Puma Test', 5, None, 3, 5)
])
def test_remove_stock(ten_stock_inventory, name, quantity, exception,
                      new_quantity, new_total):
    try:
        ten_stock_inventory.remove_stock(name, quantity)
    except (InvalidQuantityException, NoSpaceException, ItemNotFoundException) as inst:
        assert isinstance(inst, type(exception))
        assert inst.args == exception.args
    else:
        assert ten_stock_inventory.stocks[name]['quantity'] == new_quantity
        assert ten_stock_inventory.total_items == new_total

И в нашем inventory.py файл сначала мы создаем новое исключение для тех случаев, когда пользователи пытаются изменить несуществующий запас:

class ItemNotFoundException(Exception):
    pass

А затем мы добавим этот метод в наш Инвентарь класс:

def remove_stock(self, name, quantity):
    if quantity <= 0:
        raise InvalidQuantityException(
            'Cannot remove a quantity of {}. Must remove at least 1 item'.format(quantity))
    if name not in self.stocks:
        raise ItemNotFoundException(
            'Could not find {} in our stocks. Cannot remove non-existing stock'.format(name))
    if self.stocks[name]['quantity'] - quantity <= 0:
        raise InvalidQuantityException(
            'Cannot remove these {} items. Only {} items are in stock'.format(
                quantity, self.stocks[name]['quantity']))
    self.stocks[name]['quantity'] -= quantity
    self.total_items -= quantity

При запуске pytest вы должны увидеть, что интеграционный тест и все остальные проходят!

Вывод

Разработка на основе тестирования-это процесс разработки программного обеспечения, в котором тесты используются для руководства проектированием системы. TDD предписывает, что для каждой функции, которую мы должны реализовать, мы пишем тест, который терпит неудачу, добавляем наименьшее количество кода, чтобы сделать тест проходным, и, наконец, рефакторинг этого кода, чтобы быть чище.

Чтобы сделать этот процесс возможным и эффективным, мы использовали pytest – автоматизированный инструмент тестирования. С помощью pytest мы можем создавать сценарии тестов, экономя время от необходимости вручную тестировать наш код при каждом изменении.

Модульные тесты используются для обеспечения того, чтобы отдельный модуль вел себя так, как ожидалось, в то время как интеграционные тесты гарантируют, что набор модулей взаимодействует так, как мы их ожидаем. Как инструмент pytest , так и методология TDD позволяют использовать оба типа тестов, и разработчикам рекомендуется использовать оба.

С TDD мы вынуждены думать о входах и выходах нашей системы и, следовательно, о ее общем дизайне. Написание тестов дает дополнительные преимущества, такие как повышенная уверенность в функциональности нашей программы после внесения изменений. TDD требует интенсивного итеративного процесса, который может быть эффективным при использовании автоматизированного набора тестов, такого как pytest . С помощью таких функций, как светильники и параметризованные функции, мы можем быстро писать тестовые случаи в соответствии с нашими требованиями.