Syntax highlighting of python/pytest

# Pytest

[TOC]

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

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

[Homepage](https://docs.pytest.org/en/latest/)

[Plugin List](https://docs.pytest.org/en/latest/reference/plugin_list.html)


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

```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
```

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

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

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


### assert

```py
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()`

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

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

Проверяем исключение
```py
def test_exception():
	with pytest.raises(TypeError):
		foo.bar()
```
Проверяем исключение и соответствие сообщения в исключении заданному шаблону
```py
def test_raises_with_info():
	match_regex = 'missing 1 .* positional arguments'
	with pytest.raises(TypeError, match=metch_regex):
		foo.bar()
```

## Фикстуры

Примитивная фикстура
```py
import pytest

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

def test_some_data(some_data):
	assert some_data == 42
```

### Fixture Scope

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

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


### conftest.py

Общий фикстуры для файлов в директории размещаем в файле *conftest.py*
```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`

```py
@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`

```py
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](https://docs.pytest.org/en/7.1.x/how-to/tmp_path.html)

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

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

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

```py
@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)* - пропустить тест с произвольной прочиний

```py
@pytest.mark.skip(reason="Some reason")
def test_lest_than():
	assert foo() < bar()
```

*@pytest.mark.skipif(confition, ...)* - пропустить тест при некотором условии
```py
@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. Хорошая альтернатива комментированию.
```py
@pytest.mark.xfail(reason='test 123')
def test_foo():
    assert 1 == 0
```

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

Примеры маркеров
```py
@pytest.mark.smoke
```
Описываем свои маркеры в *pytest.ini*, чтобы `pytest` не сыпал предупреждения:
```ini
# pytest.ini
# ...
markers =
	smoke: subset of tests
```
Вызов тестов, помеченных маркером *smoke*
```bash
pytest -m smoke
```

Маркер уровня файла
```py
import pytest
pytestmark = pytest.mark.finish
```

Маркер уровня класса
```py
@pytest.mark.smoke
class TestFinish:
	def test_foo():
		assert True
```

Маркер уровня параметров:
```py
@pytest.mark.parametrize(
	"start_state",
	[
		"todo",
		pytest.param("in prog", marks=pytest.mark.smoke),
		"done",
		],
	)
def test_finish(start_state):
	assert foo(start_state)
```

Несколько маркеров для теста
```py
@pytest.mark.smoke
@pytest.mark.exception
def test_foo():
	assert True
```

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

```bash
pytest -m "finish and exception"
pytest -m "finish and not exception"
pytest -m "(exception or smoke) and (not finish)"
```

## Изоляция

Mocking an Attribute

```py
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

```py
@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/'
```