Разработка через тестирование с помощью pytest

Введение Хорошее программное обеспечение - это проверенное программное обеспечение. Тестирование нашего кода может помочь нам выявить ошибки или нежелательное поведение. Разработка через тестирование (TDD) - это практика разработки программного обеспечения, которая требует от нас поэтапного написания тестов для функций, которые мы хотим добавить. Он использует автоматизированные наборы для тестирования, такие как pytest [https://docs.pytest.org/en/latest/] - платформу тестирования для программ Python. * Автоматическое тестирование * Модуль pytest * Что такое разработка через тестирование? * Зачем использовать TDD для создания приложений?

Вступление

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

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

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

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

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

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

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

Модуль pytest

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

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

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

Что такое разработка через тестирование?

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

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

Этот процесс обычно называют циклом «красный-зеленый-рефакторинг»:

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

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

Зачем использовать 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 раз запустим 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 и квадратным корнем из числа.

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

Давайте обновим 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 мы получим знакомую NameError теста NameError, поскольку мы еще не определили функцию. В наш 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() было присвоено значение с плавающей запятой, выдаст ли она ошибку? Наш is_prime() не применяет правило, согласно которому простое число должно быть натуральным числом, он только проверяет, что оно больше 1.

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

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

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

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

Функции и планирование тестирования

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

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

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

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

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

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

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

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

Первые тесты

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

 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 с параметром limit, который по умолчанию равен 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 . В целях тестирования мы можем уменьшить лимит инвентаря до 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 

Вы заметите, что объект акций был инициализирован в функции __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 ошибкой, поскольку 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 

Затем мы добавляем этот метод в наш класс Inventory

 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 . Благодаря таким функциям, как фикстуры и параметризованные функции, мы можем быстро писать тестовые примеры в соответствии с нашими требованиями.

comments powered by Disqus

Содержание