Упражнения

Завершено

В этом упражнении вы будете использовать 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. Добавление файла с тестами для этого упражнения

  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 для любого другого значения.

  2. В том же файле добавьте тесты для 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
    
  3. Затем добавьте еще один тест со значениями 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 возвращаемых значений.

  4. Наконец, добавьте параметризованный тест для значений, которые функция не должна принимать. Используйте 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. Переработайте установку и демонтаж в фикстуру

  1. Добавьте тест на основе класса в файл 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, или вызвать взаимное влияние тестов друг на друга.

  2. Добавьте средство на уровне модуля, использующее встроенное 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() фикстуре используется фикстура pytest tmp_path, которая предоставляет уникальный временный каталог pathlib.Path для каждого вызова тестовой функции, включая каждый параметризованный случай. Pytest управляет временным каталогом, поэтому тест не зависит от жестко закодированного пути к файлу.

  3. Удалите предыдущий 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 ==============================

Все тесты должны проходить без ошибок.