Pytest
pytest - фреймворк для тестирования приложений
Полезные ссылки
Полезные команды и опции
# --- запуск тестов из заданного файла или файлов pytest tests/test_one.py test/test_two.py # --- запуск тестов из заданной директории pytest tests/foo/bar # --- запуск конкретных методов pytest tests/test_one.py::test_foo # --- выводить подробную информацию pytest -v pytest --verbose # --- не выводить трейс pytest -tb=no # --- выводить последовательность отработки тестов и фикстур pytest --setup-show # --- включаем стандартный вывод из тестовых функций pytest -s # --- строгие маркеры: все используемые маркеры должны быть описаны в pytest.ini pytest --strict-markers # --- отобразить причину, почему тест не прошел (fail, error, skip, xfail, xpass) pytest -ra
Конфигурация
# pytest.ini
[pytest]
testpaths = tests
pythonpath = .
markers =
smoke: subset of tests
addopts =
--strict-markers
--strict-config
-ra
Написание тестов
assert
assert something assert not something assert a == b assert a != b assert a is None assert is not None assert a <= b
pytest.fail()
Если по какой-то причине не получается применить assert, то можно использовать pytest.fail()
import pytest
def test_with_fail():
# ...
if c1 != c2:
pytest.fail("thet don't match")
Исключения
Проверяем исключение
def test_exception():
with pytest.raises(TypeError):
foo.bar()
Проверяем исключение и соответствие сообщения в исключении заданному шаблону
def test_raises_with_info():
match_regex = 'missing 1 .* positional arguments'
with pytest.raises(TypeError, match=metch_regex):
foo.bar()
Фикстуры
Примитивная фикстура
import pytest
@pytest.fixture()
def some_data():
return 42
def test_some_data(some_data):
assert some_data == 42
Fixture Scope
# --- фикстура уровня модуля
@pytest.fixture(scope="module")
def some_data():
return 42
scope='function - запускается перед каждой тестовой функцией. Значение по умолчанию.
scope='class' - запускается один раз перед тестовым классом.
scope='module' - запускается один раз перед всеми тестами модуля.
scope='session' - запускается только один раз.
conftest.py
Общий фикстуры для файлов в директории размещаем в файле conftest.py
# tests/foo/conftest.py
@pytest.fixture(scope='session')
def some_data():
return 42
# tests/foo/test_one.py
def test_foo_one(some_date):
assert some_data == 42
# tests/foo/test_two.py
def test_foo_two(some_date):
assert some_data == 42
Автоиспользование
Фикстура может быть запущена автоматом, если добавить аргумент autouse=True
@pytest.fixture(autouse=True, scope='session'):
def footer_session_scope():
'''Показываем время в конце сессии'''
yield
now = time.time()
print('--')
print(time.strftime('%d %b %X', time.localtime(now)))
Переименование фикстур
Случается так, что хочется иметь фикстуру, но имя уже занято. Ситуацию можно исправить с помощью аргумента name
import pytest
from foo import app
@pytest.fixture(scope='session', name='app')
def _app():
yield app()
def test_uses_app(app):
assert app.bar == 'bar'
Встроенные фикстуры
tmp_path - возвращает pathlib.Path, который указывает на временную директорию (function scope).
tmp_path_factory - возвращает объект TempPathFactory (session scope). См. How to use temporary directories and files in tests
capsys - управление вводом-выводом из тестов. См. How to capture stdout/stderr output.
monkeypatch - модификация (мутация) объекта или окружения. См. How to monkeypatch/mock modules and environments
Параметризация
@pytest.mark.parametrize(
'a, b, c',
[(1, 2, 3), (2, 3, 5)]
)
def test_sum(a, b, c):
assert a + b == c
Существуют другие способы: параметризация фикстур, pytest_generate_tests.
Маркеры
Встроенные маркеры
@pytest.mark.skip(reason=None) - пропустить тест с произвольной прочиний
@pytest.mark.skip(reason="Some reason")
def test_lest_than():
assert foo() < bar()
@pytest.mark.skipif(confition, ...) - пропустить тест при некотором условии
@pytest.mark.skipif(
parse(somemodule.__version__).major < 2,
reason='version not supported'
)
def test_less_than():
assert foo() < bar()
@pytest.mark.xfail(condition, ...) - этот тест ожидаемо падает. pytest посветит желтым, но завершит работу с 0. Хорошая альтернатива комментированию.
@pytest.mark.xfail(reason='test 123')
def test_foo():
assert 1 == 0
Кастомные маркеры
Примеры маркеров
@pytest.mark.smoke
Описываем свои маркеры в pytest.ini, чтобы pytest не сыпал предупреждения:
# pytest.ini
# ...
markers =
smoke: subset of tests
Вызов тестов, помеченных маркером smoke
pytest -m smoke
Маркер уровня файла
import pytest pytestmark = pytest.mark.finish
Маркер уровня класса
@pytest.mark.smoke
class TestFinish:
def test_foo():
assert True
Маркер уровня параметров:
@pytest.mark.parametrize(
"start_state",
[
"todo",
pytest.param("in prog", marks=pytest.mark.smoke),
"done",
],
)
def test_finish(start_state):
assert foo(start_state)
Несколько маркеров для теста
@pytest.mark.smoke
@pytest.mark.exception
def test_foo():
assert True
Комбинирование маркеров при вызове pytest
pytest -m "finish and exception" pytest -m "finish and not exception" pytest -m "(exception or smoke) and (not finish)"
Изоляция
Mocking an Attribute
from unittest import mock
# ...
def test_mock_version():
with mock.patch.object(cards, "__version__", "1.2.3"):
result = runner.invoke(app, ["version"])
assert result.stdout.rstrip() == "1.2.3"
Mocking a Class and Methods
@pytest.fixture()
def mock_cardsdb():
with mock.patch.object(cards, "CardsDB", autospec=True) as CardsDB:
yield CardsDB.return_value
def test_mock_path(mock_cardsdb):
mock_cardsdb.path.return_value = '/foo/'
result = runner.invoke(app, ['config'])
assert result.stdout.rstrip() == '/foo/'
