Упражнения
В этом упражнении вы будете использовать pytest с @pytest.mark.parametrize для тестирования функции, включая случаи, ожидающие исключения. Затем вы будете рефакторинговать тест на основе классов из стиля setup_method() xUnit и teardown_method() подхода к светильнику. Использование параметризации и фикстур помогает создавать тесты, охватывающие больше случаев с меньшим повторением и более четкой организацией.
Справку по настройке см. в документации по Python о виртуальных средах, в документации по pip — по pip install, а также в документации по pytest — по установке pytest. Дополнительные сведения о функциях pytest в этом упражнении см. в документации pytest по параметризации функций тестирования, утверждения о ожидаемых исключениях, исправлениях, настройке xUnit и временных каталогах и файлах.
Перед началом работы
Используйте Python версии 3.10 или более поздней версии, как описано в предварительных требованиях модуля. Если у вас уже есть виртуальная среда с установленным и активированным pytest, перейдите к шагу 1. В противном случае создайте и активируйте виртуальную среду в папке, в которой вы создадите test_advanced.py.
В Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
В macOS, Linux или подсистема Windows для Linux:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
python -m pytest --version
Команда версии должна распечатать версию pytest, аналогичную приведенным ниже выходным данным:
pytest x.y.z
Команды установки не закрепляют pytest в определенной версии. Pip использует метаданные пакета для выбора стабильного выпуска pytest, совместимого с версией Python в виртуальной среде.
Используйте команду python -m pytest во всем этом упражнении, чтобы pytest запускался из активной среды. Если Windows PowerShell блокирует Activate.ps1, изучите документацию Python venv по параметру политики выполнения PowerShell или запустите Python из виртуальной среды напрямую для выполнения команд pip и pytest. Рассмотрим пример.
.\.venv\Scripts\python -m pip install --upgrade pip
.\.venv\Scripts\python -m pip install pytest
.\.venv\Scripts\python -m pytest -v test_advanced.py
Шаг 1. Добавление файла с тестами для этого упражнения
В той же папке, в которой вы запустите pytest, создайте тестовый файл с именем test_advanced.py. Не помещайте файл в папку .venv . Добавьте следующий код:
import pytest def str_to_bool(string): normalized = string.lower() if normalized in ["yes", "y", "1"]: return True if normalized in ["no", "n", "0"]: return False raise ValueError(f"Cannot convert {string!r} to a boolean")Функция
str_to_bool()принимает строку в качестве входных данных. Он возвращаетTrue, если значение распознано как истинное; возвращаетFalse, если значение распознано как ложное; и вызываетValueErrorдля любого другого значения.В том же файле добавьте тесты для
str_to_bool()функции. Сначала используйте@pytest.mark.parametrizeдля проверки истинных значений:@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"]) def test_str_to_bool_true(string): assert str_to_bool(string) is TrueЗатем добавьте еще один тест со значениями false:
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"]) def test_str_to_bool_false(string): assert str_to_bool(string) is FalseТеперь существуют две тестовые функции, охватывающие репрезентативные входные данные для
TrueиFalseвозвращаемых значений.Наконец, добавьте параметризованный тест для значений, которые функция не должна принимать. Используйте
pytest.raises()в качестве диспетчера контекста, чтобы тест прошел только приstr_to_bool()возникновении ожидаемого исключения.@pytest.mark.parametrize("string", ["maybe", "2"]) def test_str_to_bool_invalid(string): with pytest.raises(ValueError, match="Cannot convert"): str_to_bool(string)Аргумент
matchиспользует регулярное выражение для проверки сообщения об исключении. Здесь тест проверяет только стабильную часть сообщения, поэтому он не зависит от точного недопустимого значения.
Примечание.
Для простоты это упражнение сохраняет тестируемый код и тесты в одном файле. В реальных проектах Python код приложения и тесты обычно разделяются на разные файлы и каталоги, такие как пакет src/ и каталог tests/.
Шаг 2. Запустите тесты и ознакомьтесь с отчетом
После добавления тестов запустите pytest и проверьте выходные данные. Используйте флаг расширенной детализации (-v), чтобы увидеть каждое входное значение, которое рассматривается как отдельный тест.
В Windows PowerShell:
python -m pytest -v test_advanced.py
На macOS, Linux или подсистема Windows для Linux:
python -m pytest -v test_advanced.py
Выходные данные должны быть похожи на следующий отчет:
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 10 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 10%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 20%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 30%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 40%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 50%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 60%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 70%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 80%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 90%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [100%]
============================== 10 passed in 0.01s ==============================
Ваша операционная система, версия Python, версия pytest, путь к исполняемому файлу, корневой каталог, строки подключаемого модуля и кэша, форматирование хода выполнения коллекции, перенос строк и время выполнения могут отличаться. Хотя вы написали только три тестовых функции, pytest собрал 10 тестов, так как каждое параметризованное входное значение выполняется в качестве собственного тестового случая.
Шаг 3. Переработайте установку и демонтаж в фикстуру
Добавьте тест на основе класса в файл test_advanced.py . Этот тест использует xUnit-style
setup_method()иteardown_method()методы для создания и удаления файла вокруг каждого метода теста:from pathlib import Path class TestFile: def setup_method(self): self.path = Path("done") self.path.write_text("1", encoding="utf-8") def teardown_method(self): self.path.unlink(missing_ok=True) def test_done_file(self): contents = self.path.read_text(encoding="utf-8") assert contents == "1"Этот тест может пройти, но записывает фиксированное имя файла в текущем рабочем каталоге и требует отдельного кода очистки. Если тестовый запуск прерывается или тесты выполняются одновременно, например с локальными или перекрывающимися локальными запусками
pytest-xdist, этот подход может оставить устаревшие файлы, перезаписать существующий файл, названный done, или вызвать взаимное влияние тестов друг на друга.Добавьте средство на уровне модуля, использующее встроенное
tmp_pathсредство для создания временного файла. На следующем шаге вы обновите класс, чтобы использовать эту фикстуру вместо методов установки и разрушения. Для удобства чтения поместите этот модуль на уровне модуля после параметризованныхstr_to_boolтестов и перед классомTestFile. Pytest обнаруживает модули на уровне модуля независимо от того, где они отображаются в модуле, поэтому положение предназначено для пользователей, а не pytest:@pytest.fixture def tmp_file(tmp_path): def write(): path = tmp_path / "done" path.write_text("1", encoding="utf-8") return path return writeВ
tmp_file()фикстуре используется фикстура pytesttmp_path, которая предоставляет уникальный временный каталогpathlib.Pathдля каждого вызова тестовой функции, включая каждый параметризованный случай. Pytest управляет временным каталогом, поэтому тест не зависит от жестко закодированного пути к файлу.Удалите предыдущий
TestFileкласс, включая егоsetup_method()иteardown_method()методы, и замените его на эту окончательную версию, которая использует фикстуру вместо методов установки и удаления:class TestFile: def test_done_file(self, tmp_file): path = tmp_file() contents = path.read_text(encoding="utf-8") assert contents == "1"Вы также можете удалить импорт
from pathlib import Path, добавленный на предыдущем шаге, так как тест больше не создает объектPathнапрямую.
Проверьте свою работу
К настоящему моменту должен быть файл Python с именем test_advanced.py который содержит:
- Функция
str_to_bool(), принимающая строку и возвращающая логическое значение для распознаваемых истинных и ложных значений. - Три параметризованных теста для функции
str_to_bool(): один проверяет значенияTrue, другой проверяет значенияFalse, и еще один проверяет, что недопустимые значения вызываютValueError. - Пользовательское средство pytest, использующее встроенное
tmp_pathсредство для создания временного готового файла с некоторым содержимым. -
TestFileОдин класс с одним методом, использующим настраиваемоеtmp_fileсредство для создания и чтения файла. - В
from pathlib import Pathокончательной версии нет импорта и нет методовsetup_method()/teardown_method().
Окончательный файл должен выглядеть следующим образом:
import pytest
def str_to_bool(string):
normalized = string.lower()
if normalized in ["yes", "y", "1"]:
return True
if normalized in ["no", "n", "0"]:
return False
raise ValueError(f"Cannot convert {string!r} to a boolean")
@pytest.mark.parametrize("string", ["Y", "y", "1", "YES"])
def test_str_to_bool_true(string):
assert str_to_bool(string) is True
@pytest.mark.parametrize("string", ["N", "n", "0", "NO"])
def test_str_to_bool_false(string):
assert str_to_bool(string) is False
@pytest.mark.parametrize("string", ["maybe", "2"])
def test_str_to_bool_invalid(string):
with pytest.raises(ValueError, match="Cannot convert"):
str_to_bool(string)
@pytest.fixture
def tmp_file(tmp_path):
def write():
path = tmp_path / "done"
path.write_text("1", encoding="utf-8")
return path
return write
class TestFile:
def test_done_file(self, tmp_file):
path = tmp_file()
contents = path.read_text(encoding="utf-8")
assert contents == "1"
Снова запустите тестовый файл.
В Windows PowerShell:
python -m pytest -v test_advanced.py
На macOS, Linux или подсистема Windows для Linux:
python -m pytest -v test_advanced.py
Окончательный отчет должен показать 11 собранных тестов: 10 случаев из параметризованных тестов и одного теста на основе класса.
============================= test session starts ==============================
platform ... -- Python 3.x.y, pytest-x.y.z, pluggy-x.y.z -- ...
rootdir: ...
...
collected 11 items
test_advanced.py::test_str_to_bool_true[Y] PASSED [ 9%]
test_advanced.py::test_str_to_bool_true[y] PASSED [ 18%]
test_advanced.py::test_str_to_bool_true[1] PASSED [ 27%]
test_advanced.py::test_str_to_bool_true[YES] PASSED [ 36%]
test_advanced.py::test_str_to_bool_false[N] PASSED [ 45%]
test_advanced.py::test_str_to_bool_false[n] PASSED [ 54%]
test_advanced.py::test_str_to_bool_false[0] PASSED [ 63%]
test_advanced.py::test_str_to_bool_false[NO] PASSED [ 72%]
test_advanced.py::test_str_to_bool_invalid[maybe] PASSED [ 81%]
test_advanced.py::test_str_to_bool_invalid[2] PASSED [ 90%]
test_advanced.py::TestFile::test_done_file PASSED [100%]
============================== 11 passed in 0.01s ==============================
Все тесты должны проходить без ошибок.