Syntax highlighting of c5400ae ~( python/pytest)

= Pytest =

<<TableOfContents()>>

pytest - фреймворк для тестирования приложений

== Полезные ссылки ==

[[https://docs.pytest.org/en/latest/|Homepage|class=" moin-https"]]

[[https://docs.pytest.org/en/latest/reference/plugin_list.html|Plugin List|class=" moin-https"]]

== Полезные команды и опции ==

{{{#!highlight bash
# --- запуск тестов из заданного файла или файлов
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

}}}

== Конфигурация ==

{{{#!highlight ini
# pytest.ini
[pytest]
testpaths = tests
pythonpath = .
markers =
    smoke: subset of tests
addopts =
    --strict-markers
    --strict-config
    -ra

}}}

== Написание тестов ==

=== assert ===

{{{#!highlight python
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()`

{{{#!highlight python
import pytest
def test_with_fail():
    # ...
    if c1 != c2:
        pytest.fail("thet don't match")

}}}

=== Исключения ===

Проверяем исключение

{{{#!highlight python
def test_exception():
    with pytest.raises(TypeError):
        foo.bar()

}}}

Проверяем исключение и соответствие сообщения в исключении заданному шаблону

{{{#!highlight python
def test_raises_with_info():
    match_regex = 'missing 1 .* positional arguments'
    with pytest.raises(TypeError, match=metch_regex):
        foo.bar()

}}}

== Фикстуры ==

Примитивная фикстура

{{{#!highlight python
import pytest

@pytest.fixture()
def some_data():
    return 42

def test_some_data(some_data):
    assert some_data == 42

}}}

=== Fixture Scope ===

{{{#!highlight python
# --- фикстура уровня модуля
@pytest.fixture(scope="module")
def some_data():
    return 42

}}}

`scope='function` - запускается перед каждой тестовой функцией. Значение по умолчанию.
`scope='class'` - запускается один раз перед тестовым классом.
`scope='module'` - запускается один раз перед всеми тестами модуля.
`scope='session'` - запускается только один раз.

=== conftest.py ===

Общий фикстуры для файлов в директории размещаем в файле ''conftest.py''

{{{#!highlight python
# 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`

{{{#!highlight python
@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`

{{{#!highlight python
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). См. [[https://docs.pytest.org/en/7.1.x/how-to/tmp_path.html|How to use temporary directories and files in tests|class=" moin-https"]]

''capsys'' - управление вводом-выводом из тестов. См. [[https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html|How to capture stdout/stderr output|class=" moin-https"]].

''monkeypatch'' - модификация (мутация) объекта или окружения. См. [[https://docs.pytest.org/en/7.1.x/how-to/monkeypatch.html|How to monkeypatch/mock modules and environments|class=" moin-https"]]

== Параметризация ==

{{{#!highlight python
@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)'' - пропустить тест с произвольной прочиний

{{{#!highlight python
@pytest.mark.skip(reason="Some reason")
def test_lest_than():
    assert foo() < bar()

}}}

''@pytest.mark.skipif(confition, ...)'' - пропустить тест при некотором условии

{{{#!highlight python
@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. Хорошая альтернатива комментированию.

{{{#!highlight python
@pytest.mark.xfail(reason='test 123')
def test_foo():
    assert 1 == 0

}}}

=== Кастомные маркеры ===

Примеры маркеров

{{{#!highlight python
@pytest.mark.smoke

}}}

Описываем свои маркеры в ''pytest.ini'', чтобы `pytest` не сыпал предупреждения:

{{{#!highlight python
# pytest.ini
# ...
markers =
    smoke: subset of tests

}}}

Вызов тестов, помеченных маркером ''smoke''

{{{#!highlight bash
pytest -m smoke

}}}

Маркер уровня файла

{{{#!highlight python
import pytest
pytestmark = pytest.mark.finish

}}}

Маркер уровня класса

{{{#!highlight python
@pytest.mark.smoke
class TestFinish:
    def test_foo():
        assert True

}}}

Маркер уровня параметров:

{{{#!highlight python
@pytest.mark.parametrize(
    "start_state",
    [
        "todo",
        pytest.param("in prog", marks=pytest.mark.smoke),
        "done",
        ],
    )
def test_finish(start_state):
    assert foo(start_state)

}}}

Несколько маркеров для теста

{{{#!highlight python
@pytest.mark.smoke
@pytest.mark.exception
def test_foo():
    assert True

}}}

Комбинирование маркеров при вызове `pytest`

{{{#!highlight bash
pytest -m "finish and exception"
pytest -m "finish and not exception"
pytest -m "(exception or smoke) and (not finish)"

}}}

== Изоляция ==

Mocking an Attribute

{{{#!highlight python
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

{{{#!highlight python
@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/'

}}}