Pytest-это мой фреймворк для тестирования Python из-за его гибкости. Одной из его замечательных особенностей является возможность записывать хуки в различные точки выполнения набора тестов. Все они могут быть ссылками через API docs . На работе у нас могут быть сотни тестов, и иногда изменения могут привести к провалу десятков тестов. Теперь давайте не будем слишком углубляться в тему “Когда вы вносите изменения, только несколько тестов должны сломаться. Вы не должны видеть 30-40 провалов тестов.” Ну конечно, это здорово с идеальным набором тестов, но иногда у вас просто не будет этого. Или вы будете вносить изменения в промежуточное программное обеспечение, которое влияет на все вызовы, и у вас будет масса сбоев. Когда у вас есть 30-40 неудачных тестов, если не только 10, вы обнаружите, что просеиваете множество выходных данных pytest, чтобы просто найти тесты, которые потерпели неудачу. Из-за этого давайте просто подключимся к выполнению pytests и запишем все наши провалы тестов в failures.txt таким образом, мы можем очень ясно видеть, какие из наших тестов провалились.
Создайте новый virtualenv
и установите в него pytest
. Я предполагаю, что Python3.6
Это все, что вам нужно.
Напишите простой тест, который мы заставляем провалиться в файле tests.py
def test_failed(): assert False def test_passed(): assert True
Создать conftest.py
и добавьте следующий код.
import pytest @pytest.hookimpl() def pytest_sessionstart(session): print("hello")
Pytest автоматически заберет ваш крючок из conftest.py
очень похоже на то, что было бы с светильниками.
Если мы запустим $ pytest tests.py
мы можем видеть результат следующим образом. (Для краткости удалил некоторые =
)
19:45 $ pytest tests.py hello =======================...=================== test session starts
В соответствии с api docs мы видим, что это был бы подходящий крючок для создания нашего failures.txt
файл так что давайте сделаем это.
Измените крючок следующим образом
# pathlib is great from pathlib import Path from _pytest.main import Session # Let's define our failures.txt as a constant as we will need it later FAILURES_FILE = Path() / "failures.txt" @pytest.hookimpl() def pytest_sessionstart(session: Session): if FAILURES_FILE.exists(): # We want to delete the file if it already exists # so we don't carry over failures form last run FAILURES_FILE.unlink() FAILURES_FILE.touch()
Мне нравится использовать pathlib
при работе с чем-либо на пути только потому, что это намного приятнее, чем os.path
. Логика изменения довольно проста. Если файл уже существует, удалите его, а затем создайте новый.
Далее нам нужно написать крюк, чтобы добавить тестовые сбои к нашему failures.txt
файл. Согласно api docs pytest_run test_make report
был бы нашим желаемым крючком. Этот хук запускается сразу после запуска тестового набора и получает сам тестовый набор ( Item
), а также результат тестового набора ( CallInfo
). Добавьте в следующее conftest.py
# ...other imports from _pytest.nodes import Item from _pytest.runner import CallInfo # ...previous code @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item: Item, call: CallInfo): # All code prior to yield statement would be ran prior # to any other of the same fixtures defined outcome = yield # Run all other pytest_runtest_makereport non wrapped hooks result = outcome.get_result() if result.when == "call" and result.failed: try: # Just to not crash py.test reporting with open(str(FAILURE_FILE), "a") as f: f.write(result.nodeid + "\n") except Exception as e: print("ERROR", e) pass
Поскольку мы не хотим изменять сам отчет о тестировании, мы напишем наш как завернутый крючок через hookwrapper=True
. В api docs give приведен отличный пример упорядочения вызовов hook, который является более глубоким, чем мои комментарии.
В этот момент, если мы запустим $ pytest tests.py
мы видим, что наш единственный неудачный тест записывается в failures.txt
$ cat failures.txt tests.py::test_failed
Разработчики из вашей команды могут даже не заметить этот файл, поэтому давайте дадим им небольшое направление. Давайте добавим крючок для печати направлений в конце тестового запуска. Снова просматривая api docs оказывается pytest_terminal_summary
является нашим желаемым крючком.
# ... previous imports from _pytest.runner import CallInfo from _pytest.terminal import TerminalReporter # ... previous hooks @pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, exitstatus: int, config: Config ): yield print(f"Failures outputted to: {FAILURE_FILE}") print(f"to see run\ncat {FAILURE_FILE}")
В нашем случае, я думаю, имеет смысл запустить все остальные крючки до того, как мы запустим наш, чтобы мы могли убедиться, что наши направления-это последнее, что печатается на терминале. Снова мы используем hookwrapper=True
и помещаем наш код ПОСЛЕ оператора yield
. В нашем случае мы ничего не делаем с результатом yield
, поэтому мы можем просто вызвать его и назначить его ничему. С этим изменением кода запустите $ pytest tests.py
снова и снова мы видим следующее.
tests.py:3: AssertionError Failures outputted to: failures.txt to see run cat failures.txt =====================...== 1 failed, 1 passed in 0.04s=====...===
Я надеюсь, что это послужило хорошим реальным примером использования написания пользовательских крючков в pytest
. Не стесняйтесь обращаться ко мне с любыми вопросами или отзывами относительно этого поста.