Тестовые классы и методы

Завершено

Помимо написания функций тестирования, Pytest позволяет использовать классы. Как уже упоминалось, не требуется наследование и тестовые классы следуют нескольким простым правилам. Использование классов обеспечивает большую гибкость и возможность повторного использования. Как вы видите далее, Pytest держится вне пути и избегает принудительного написания тестов определенным образом.

Как и с функциями, вы по-прежнему можете писать утверждения с помощью утверждения assert.

Создание тестового класса

Давайте рассмотрим реальный сценарий, чтобы узнать, как могут помочь тестовые классы. Следующая функция проверяет, содержит ли указанный файл "да" в его содержимом. Если да, возвращается True. Если файл не существует или содержит "нет" в его содержимом, он возвращается False. Этот сценарий распространен в асинхронных задачах, использующих файловую систему для указания завершения.

Вот как выглядит функция:

import os

def is_done(path):
    if not os.path.exists(path):
        return False
    with open(path) as _f:
        contents = _f.read()
    if "yes" in contents.lower():
        return True
    elif "no" in contents.lower():
        return False

Теперь вот как класс с двумя тестами (по одному для каждого условия) в файле с именем test_files.py выглядит:

class TestIsDone:

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Внимание

Методы теста используют путь /tmp для временного тестового файла, так как его проще использовать в качестве примера. Однако если вам нужно использовать временные файлы, рассмотрите возможность безопасного создания (и удаления) библиотеки типа tempfile. Не каждая система имеет каталог /tmp, и это расположение может не быть временным в зависимости от операционной системы.

Выполнение тестов с флагом -v для повышения детализации показывает, что тесты проходят:

pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
cachedir: .pytest_cache
rootdir: /private/
collected 2 items

test_files.py::TestIsDone::test_yes PASSED                               [ 50%]
test_files.py::TestIsDone::test_no PASSED                                [100%]

============================== 2 passed in 0.00s ===============================

Хотя тесты передаются, они выглядят повторяющимися, и они также покидают файлы после завершения теста. Прежде чем мы узнаем, как улучшить их, рассмотрим вспомогательные методы в следующем разделе.

Вспомогательные методы

В тестовом классе существует несколько методов, которые можно использовать для настройки и разрыва выполнения теста. Pytest выполняет их автоматически, если они определены. Чтобы использовать эти методы, необходимо знать, что они имеют определенный порядок и поведение.

  • setup: выполняется один раз перед каждым тестом в классе.
  • teardown: выполняется один раз после каждого теста в классе.
  • setup_class: выполняется один раз перед всеми тестами в классе
  • teardown_class: выполняется один раз после всех тестов в классе

Если для работы тестов требуются аналогичные (или идентичные) ресурсы, полезно написать методы установки. В идеале тест не должен покидать ресурсы после завершения, поэтому методы очистки могут помочь в удалении тестов в таких ситуациях.

Очистка

Давайте рассмотрим обновленный тестовый класс, который очищает файлы после каждого теста:

class TestIsDone:

    def teardown(self):
        if os.path.exists("/tmp/test_file"):
            os.remove("/tmp/test_file")

    def test_yes(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("yes")
        assert is_done("/tmp/test_file") is True

    def test_no(self):
        with open("/tmp/test_file", "w") as _f:
            _f.write("no")
        assert is_done("/tmp/test_file") is False

Так как мы использовали teardown() этот метод, этот тестовый класс больше не оставляет за собой /tmp/test_file .

Настройка

Еще одно улучшение, которое мы можем сделать для этого класса, заключается в добавлении переменной, указывающей на файл. Так как файл теперь объявлен в шести местах, любые изменения в пути будут означать го изменение во всех этих местах. В этом примере показано, как выглядит класс с добавленным setup() методом, объявляющим переменную пути:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def test_yes(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        with open(self.tmp_file, "w") as _f:
            _f.write("no")
        assert is_done(self.tmp_file) is False

Пользовательские вспомогательные методы

Пользовательские вспомогательные методы можно создавать в классе. Эти методы не должны быть префиксированы именем test и не могут быть названы как методы установки или очистки. В классе TestIsDone можно автоматизировать создание временного файла в пользовательском вспомогательном приложении. Этот настраиваемый вспомогательный метод может выглядеть следующим образом:

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

Pytest не выполняет write_tmp_file() метод автоматически, а другие методы могут вызывать его непосредственно для сохранения повторяющихся задач, таких как запись в файл.

Весь класс выглядит следующим образом, после обновления методов тестирования для использования пользовательского вспомогательного средства:

class TestIsDone:

    def setup(self):
        self.tmp_file = "/tmp/test_file"

    def teardown(self):
        if os.path.exists(self.tmp_file):
            os.remove(self.tmp_file)

    def write_tmp_file(self, content):
        with open(self.tmp_file, "w") as _f:
            _f.write(content)

    def test_yes(self):
        self.write_tmp_file("yes")
        assert is_done(self.tmp_file) is True

    def test_no(self):
        self.write_tmp_file("no")
        assert is_done(self.tmp_file) is False

Когда следует использовать класс вместо функции

Не существует строгих правил относительно того, когда следует использовать класс вместо функции. Всегда рекомендуется следовать соглашениям в текущих проектах и командах, с которыми вы работаете. Ниже приведены некоторые общие вопросы, которые помогут вам определить, когда следует использовать класс:

  • Требуется ли ваш тест аналогичный вспомогательный код установки или очистки?
  • Группирование тестов вместе имеет логический смысл?
  • Есть ли хотя бы несколько тестов в наборе тестов?
  • Могут ли тесты воспользоваться общим набором вспомогательных функций?