Тест - это код, который выполняет код. Когда вы начинаете разрабатывать новую функцию для своего проекта Python, вы можете формализовать требования к ней в виде кода. Поступая таким образом, вы не только документируете то, как должен использоваться код вашей реализации, но и можете автоматически запускать все тесты, чтобы всегда быть уверенным, что ваш код соответствует вашим требованиям. Одним из таких инструментов, который помогает вам в этом, является pytest, и это, вероятно, самый популярный инструмент тестирования во вселенной Python.
Предположим, что вы написали функцию, которая проверяет адрес электронной почты. Обратите внимание, что мы упрощаем задачу и не используем регулярные выражения или проверку DNS для проверки адресов электронной почты. Вместо этого мы просто убедимся, что в проверяемой строке есть только один знак @ и только латинские символы, цифры, а также символы ., - и _.
import string
def is_valid_email_address(s):
s = s.lower()
parts = s.split('@')
if len(parts) != 2:
# Если частей больше, чем 2, т.е. знак '@' не единственный
return False
allowed = set(string.ascii_lowercase + string.digits + '.-_')
for part in parts:
if not set(part) <= allowed:
# Найдены неположенные символы
return False
return True
Теперь у нас есть некоторые утверждения в нашем коде. Например, мы утверждаем, что эти адреса электронной почты действительны:
test@example.orguser123@subdomain.example.orgjohn.doe@email.example.org
С другой стороны, мы ожидаем, что наша функция вернет False для таких адресов электронной почты, как:
not valid@example.org - тут есть пробелjohn.doe - пропущен знак ‘@’john,doe@example.org - в адресе есть запятая
Мы можем проверить, что наша функция действительно ведет себя так, как мы ожидаем:
print(is_valid_email_address('test@example.org')) # True
print(is_valid_email_address('user123@subdomain.example.org')) # True
print(is_valid_email_address('john.doe@email.example.org')) # True
print(is_valid_email_address('not valid@example.org')) # False
print(is_valid_email_address('john.doe')) # False
print(is_valid_email_address('john,doe@example.org')) # False
Эти примеры адресов электронной почты, которые мы придумываем, называются тестовыми случаями. Для каждого тестового случая мы ожидаем определенный результат. Такой инструмент, как pytest, может помочь автоматизировать тестирование этих утверждений. Запись этих утверждений может помочь вам:
- документировать, как будет использоваться ваш код
- убедиться, что будущие изменения не сломают другие части вашего программного обеспечения
- подумать о возможных крайних случаях использования ваших функциональных возможностей.
Чтобы сделать это, мы просто создадим новый файл для всех наших тестов и поместим туда несколько функций.
def test_regular_email_validates():
assert is_valid_email_address('test@example.org')
assert is_valid_email_address('user123@subdomain.example.org')
assert is_valid_email_address('john.doe@email.example.org')
def test_valid_email_has_one_at_sign():
assert not is_valid_email_address('john.doe')
def test_valid_email_has_only_allowed_chars():
assert not is_valid_email_address('john,doe@example.org')
assert not is_valid_email_address('not valid@example.org')
Итак, у нас есть два файла в каталоге нашего проекта: validator.py и test_validator.py.
Теперь мы можем просто запустить pytest из командной строки. Его вывод должен выглядеть примерно так:
======================= test session starts =========================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 3 items
test_validator.py ... [100%]
======================== 3 passed in 0.01s ==========================
Здесь pytest сообщает нам, что он нашел три тестовые функции внутри test_validator.py и что все эти функции были пройдены (на что указывают три точки ...).
Индикатор 100% дает нам хорошее чувство, поскольку мы уверены, что наш валидатор работает так, как ожидалось. Однако, как было сказано во введении, функция валидатора далека от совершенства. Так же как и наши тестовые примеры. Даже без тестирования DNS мы бы отметили адрес электронной почты типа john.doe@example как действительный, в то время как адрес типа john.doe+abc@gmail.com был бы отмечен как недействительный.
Теперь добавим эти тестовые случаи в наш test_validator.py.
...
def test_valid_email_can_have_plus_sign():
assert is_valid_email_address('john.doe+abc@gmail.com')
def test_valid_email_must_have_a_tld():
assert not is_valid_email_address('john.doe@example')
Если мы снова запустим pytest, то увидим неудачные тесты:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items
test_validator.py ...FF [100%]
=================================== FAILURES ===================================
_____________________ test_valid_email_can_have_plus_sign ______________________
def test_valid_email_can_have_plus_sign():
> assert is_valid_email_address('john.doe+abc@gmail.com')
E AssertionError: assert False
E + where False = is_valid_email_address('john.doe+abc@gmail.com')
test_validator.py:17: AssertionError
_______________________test_valid_email_must_have_a_tld_______________________
def test_valid_email_must_have_a_tld():
> assert not is_valid_email_address('john.doe@example')
E AssertionError: assert not True
E + where True = is_valid_email_address('john.doe@example')
test_validator.py:20: AssertionError
=========================== short test summary info ============================
FAILED test_validator.py::test_valid_email_can_have_plus_sign - AssertionErro...
FAILED test_validator.py::test_valid_email_must_have_a_tld - AssertionError: ...
========================= 2 failed, 3 passed in 0.05s ==========================
Обрати внимание, что мы получили два FF в дополнение к трем ... точкам, указывающим на то, что две тестовые функции не сработали.
Кроме того, мы получаем новый раздел FAILURES в выводе, который подробно объясняет, в какой момент наш тест потерпел неудачу. Это очень полезно для отладки.
Наш небольшой пример с валидатором является подтверждением важности разработки тестов.
Сначала мы написали функцию валидатора, а затем придумали для неё несколько тестовых примеров. Вскоре мы заметили, что эти тесты ни в коем случае не являются исчерпывающими. Напротив, мы упустили некоторые существенные аспекты валидации адреса электронной почты.
Возможно, вы слышали о Test Driven Development (TDD), которая выступает за прямо противоположное: Сначала нужно правильно сформулировать требования, написав тестовые примеры, и не приступать к реализации функции до того, как вы почувствуете, что охватили все тестовые случаи. Такой образ мышления всегда был хорошей идеей, но со временем он приобрел еще большее значение, поскольку сложность программных проектов возросла.
В ближайшее время я напишу еще одну статью в блоге о TDD, чтобы рассказать о нем более подробно.
Обычно настройка проекта намного сложнее, чем просто один файл с функцией валидатора в нем.
Возможно, у вас есть структура пакета Python для вашего проекта, или ваш код зависит от внешних зависимостей, таких как база данных.
Вы могли использовать термин фикстура в разных контекстах. Например, для Django web framework, фикстуры относятся к коллекции начальных данных, загружаемых в базу данных. Однако, в контексте pytest, фикстуры относятся только к функциям, выполняемым pytest до и/или после фактических тестовых функций.
Мы можем создавать такие функции с помощью декоратора pytest.fixture(). Пока что мы делаем это внутри файла test_validator.py.
import pytest
@pytest.fixture()
def database_environment():
setup_database()
yield
teardown_database()
Обрати внимание, что создание базы данных и ее уничтожение происходит в одной и том же фикстуре. Ключевое слово yield указывает на ту часть, где pytest выполняет фактические тесты.
Чтобы фикстура действительно использовалась одним из ваших тестов, вы просто добавляете имя фикстуры в качестве аргумента, как показано ниже (по-прежнему в test_validator.py):
def test_world(database_environment):
assert 1 == 1
Вместо использования yield, функция приспособления может также возвращать произвольные значения:
import pytest
@pytest.fixture()
def my_fruit():
return "apple"
Опять же, запрос этого приспособления из тестовой функции осуществляется путем предоставления имени приспособления в качестве параметра:
def test_fruit(my_fruit):
assert my_fruit == "apple"
pytest может читать свою конфигурацию, специфичную для проекта, из одного из этих файлов:
pytest.initox.inisetup.cfg
Какой файл использовать, зависит от того, какие другие инструменты вы можете использовать в своем проекте. Если вы упаковали свой проект, вам следует использовать файл setup.cfg. Если вы используете tox для тестирования вашего кода в различных средах, вы можете поместить конфигурацию pytest в файл tox.ini. Файл pytest.ini можно использовать, если вы не хотите использовать никаких дополнительных инструментов, кроме pytest.
Конфигурационный файл выглядит одинаково для каждого из этих трех типов файлов:
[pytest]
addopts = -rsxX -l --tb=short --strict
Если ты используешь файл setup.cfg, единственное отличие заключается в том, что вы должны префикс секции [pytest] с tool:, как показано ниже:
[tool:pytest]
addopts = -rsxX -l --tb=short --strict
Каждая папка с тестовыми файлами может содержать файл conftest.py, который читается pytest. Это хорошее место для размещения ваших пользовательских фикстур, так как они могут быть общими для разных тестовых файлов.
Файл(ы) conftest.py может(ют) изменять поведение pytest в зависимости от проекта.
Помимо общих фикстур, вы можете разместить внешние хуки и плагины или модификаторы для PATH, используемого pytest для обнаружения тестов и кода реализации.
Во время разработки, в основном, когда вы пишете тесты перед реализацией, pytest может быть полезным инструментом для отладки.
Мы рассмотрим наиболее полезные опции командной строки.
Если вы хотите запустить только один конкретный тест, вы можете сослаться на него через файл test_, в котором он находится, и имя функции:
pytest test_validator.py::test_regular_email_validates
Иногда все, что вам нужно - это список коллекции тестов, а не выполнение тестовых функций.
pytest --collect-only
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items
<Module test_validator.py>
<Function test_regular_email_validates>
<Function test_valid_email_has_one_at_sign>
<Function test_valid_email_has_only_allowed_chars>
<Function test_valid_email_can_have_plus_sign>
<Function test_valid_email_must_have_a_tld>
========================== 5 tests collected in 0.01s ==========================
Вы можете заставить pytest прекратить выполнение последующих тестов после неудачного:
Если вы хотите запустить только те тесты, которые не прошли в прошлый раз, вы можете сделать это с помощью флага --lf:
Если мы создадим более сложную тестовую функцию с некоторыми локальными переменными, мы можем указать pytest отобразить эти локальные переменные с помощью флага -l.
Давайте перепишем нашу тестовую функцию следующим образом:
...
def test_valid_email_can_have_plus_sign():
email = 'john.doe+abc@gmail.com'
assert is_valid_email_address('john.doe+abc@gmail.com')
...
Тогда,
даст нам такой результат:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items
test_validator.py ...FF [100%]
=================================== FAILURES ===================================
_____________________ test_valid_email_can_have_plus_sign ______________________
def test_valid_email_can_have_plus_sign():
email = 'john.doe+abc@gmail.com'
> assert is_valid_email_address('john.doe+abc@gmail.com')
E AssertionError: assert False
E + where False = is_valid_email_address('john.doe+abc@gmail.com')
email = 'john.doe+abc@gmail.com'
test_validator.py:18: AssertionError
_______________________test_valid_email_must_have_a_tld_______________________
def test_valid_email_must_have_a_tld():
> assert not is_valid_email_address('john.doe@example')
E AssertionError: assert not True
E + where True = is_valid_email_address('john.doe@example')
test_validator.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_validator.py::test_valid_email_can_have_plus_sign - AssertionErro...
FAILED test_validator.py::test_valid_email_must_have_a_tld - AssertionError: ...
========================= 2 failed, 3 passed in 0.09s ==========================
Существует отладчик командной строки, называемый pdb, который встроен в Python. Вы можете использовать pytest для отладки кода вашей тестовой функции.
Если вы запустите pytest с --pdb, он запустит сессию отладки pdb сразу после того, как в вашем тесте возникнет исключение. В большинстве случаев это не очень полезно, поскольку вы можете захотеть проверить каждую строку кода, предшествующую возникшему исключению.
Другой вариант - флаг --trace для pytest, который будет устанавливать точку останова в первой строке каждой тестовой функции. Это может стать немного неудобным, если у вас много тестов. Поэтому для целей отладки хорошей комбинацией будет --lf --trace, которая запустит сессию отладки с pdb в начале последнего неудачного теста:
pytest --lf --trace
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example, configfile: pytest.ini
collected 2 items
run-last-failure: rerun previous 2 failures
test_validator.py
>>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> /Users/bascodes/Code/blogworkspace/pytest-example/test_validator.py(17)test_valid_email_can_have_plus_sign()
-> email = 'john.doe+abc@gmail.com'
(Pdb)
В современных программных проектах программное обеспечение разрабатывается в соответствии с принципами Test Driven Development и доставляется через конвейер непрерывной интеграции / непрерывного развертывания, включающий автоматизированное тестирование.
Типичная установка заключается в том, что коммиты в ветку main/master отклоняются, если не пройдены все тестовые функции.
Если вы хотите узнать больше об использовании pytest в среде CI/CD, оставайтесь с нами, так как я планирую новую статью на эту тему.
Официальная документация по pytest находится здесь: https://docs.pytest.org/en/stable/
Примечание:
Это перепост статьи Баса Штайнса, сделанный с его разрешения. Рекомендую посетить его сайт или X @bascodes.