Spec-Zone.ru › Ansible

Руководство по UTHelper

Введение

UTHelper был создан для сокращения шаблонного кода, используемого в модульных тестах модулей. Изначально он был написан для обработки тестов модулей, запускающих внешние команды с помощью AnsibleModule.run_command(). На момент написания (февраль 2025 года) это остаётся единственным типом тестов, для которого можно использовать UTHelper, но в будущем планируется добавить поддержку других типов взаимодействия.

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

  • test_apk.py — очень простой пример
  • test_bootc_manage.py — здесь больше тестовых случаев, но обратите внимание, сколько кода в них повторяется.
  • test_modprobe.py — здесь 15 тестов, но для этого объявлено 8 классов, в которых повторяется довольно много кода.

Как можно заметить, эти тесты выполняются непоследовательно: в итоге все они делают одно и то же, но каждый написан совершенно по-разному.

Цели UTHelper:

  • предоставить единообразный способ определения модульных тестов
  • сократить код до абсолютного минимума и
  • описывать тесты как данные
  • позволить задавать тестовые случаи не только в виде структуры данных Python, но и в виде содержимого YAML

Краткое руководство

Для использования UTHelper в тестовом модуле потребуется лишь минимальный объём кода:

# tests/unit/plugin/modules/test_ansible_module.py
from ansible_collections.community.general.plugins.modules import ansible_module
from .uthelper import UTHelper, RunCommandMock


UTHelper.from_module(ansible_module, __name__, mocks=[RunCommandMock])

Затем в файле спецификации тестов укажите:

# tests/unit/plugin/modules/test_ansible_module.yaml
test_cases:
  - id: test_ansible_module
    flags:
      diff: true
    input:
      state: present
      name: Roger the Shrubber
    output:
      shrubbery:
        looks: nice
        price: not too expensive
      changed: true
      diff:
        before:
          shrubbery: null
        after:
          shrubbery:
            looks: nice
            price: not too expensive
    mocks:
      run_command:
        - command: [/testbin/shrubber, --version]
          rc: 0
          out: "2.80.0\n"
          err: ''
        - command: [/testbin/shrubber, --make-shrubbery]
          rc: 0
          out: 'Shrubbery created'
          err: ''

Примечание

Если вы предпочитаете выбрать для тестовых случаев другой YAML-файл или определить их непосредственно на Python, можно воспользоваться вспомогательными методами UTHelper.from_file() и UTHelper.from_spec() соответственно. Подробнее см. ниже.

Использование UTHelper

Тестовый модуль

UTHelper предназначен исключительно для модульных тестов. Для его использования импортируйте класс .uthelper.UTHelper. Как упоминалось в разных разделах этого руководства, существует три различных способа загрузки тестовых случаев.

См. также

Подробные сведения об API трёх способов см. в справочнике по классу UTHelper ниже.

Самый простой и рекомендуемый способ использования UTHelper — буквально пример, приведённый выше. Реальный пример см. в файле test_gconftool2.py.

Метод from_module() определит имя файла тестового модуля (в примере выше — tests/unit/plugins/modules/test_gconftool2.py) и выполнит поиск tests/unit/plugins/modules/test_gconftool2.yaml (или .yml, если первый файл не найден). В этом файле он ожидает найти спецификацию тестов в формате YAML, соответствующую описанной ниже структуре LINK LINK LINK.

Если вы предпочитаете считывать спецификации тестов из другого файла, используйте from_file(), передав дескриптор YAML-файла.

Если по какой-либо причине вы предпочитаете или вам необходимо передать структуру данных вместо работы с YAML-файлами, используйте метод from_spec(). Реальный пример можно найти в файле test_snap.py.

Спецификация тестов

Структура данных спецификации тестов описана ниже.

Верхний уровень

На верхнем уровне допускаются два ключа:

  • anchors: dict

    Необязательный. Место для определения YAML-якорей, которые можно повторно использовать в тестовых случаях. Содержимое этого ключа не считывается непосредственно Test Helper.

  • test_cases: list

    Обязательный. Список тестовых случаев; описание см. ниже.

Тестовые случаи

Тестовые случаи задаются пятью элементами:

  • id: str

    Обязательный. Используется для идентификации тестового случая.

  • flags: dict

    Необязательный. Флаги, управляющие поведением тестового случая. Все флаги необязательны. Допустимые флаги:

    • check: bool: задайте значение true, если модуль должен выполняться в режиме проверки.
    • diff: bool: задайте значение true, если модуль должен выполняться в режиме сравнения различий.
    • skip: str: указывает, что тестовый случай нужно пропустить, и задаёт сообщение для pytest.skip().
    • xfail: str: указывает, что ожидается сбой тестового случая, и задаёт сообщение для pytest.xfail().
  • input: dict

    Необязательный. Параметры модуля Ansible; значение может быть пустым.

  • output: dict

    Необязательный. Ожидаемые возвращаемые значения модуля Ansible. Все указанные здесь имена RV должны присутствовать в выводе модуля, но не обязательно указывать все RV из вывода. Можно включить специальные RV, такие как changed и diff. Значение может быть пустым.

  • mocks: dict

    Необязательный. Имитируемые взаимодействия; пока поддерживается только run_command. Каждый ключ этого словаря соответствует подклассу TestCaseMock, а его структура определяется реализацией подкласса TestCaseMock. Имена всех ключей должны быть указаны в snake case, например run_command. Подкласс TestCaseMock отвечает за определение имени, используемого в спецификации тестов. Структура этой спецификации зависит от реализующего её класса. Подробнее о реализации RunCommandMock см. ниже.

Пример с использованием YAML

Для чтения спецификаций тестов из YAML-файла рекомендуется использовать UTHelper. Ниже приведён пример того, как выглядит такая спецификация (фрагмент из test_opkg.yaml):

---
anchors:
  environ: &env-def {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false}
test_cases:
  - id: install_zlibdev
    input:
      name: zlib-dev
      state: present
    output:
      msg: installed 1 package(s)
    mocks:
      run_command:
        - command: [/testbin/opkg, --version]
          environ: *env-def
          rc: 0
          out: ''
          err: ''
        - command: [/testbin/opkg, list-installed, zlib-dev]
          environ: *env-def
          rc: 0
          out: ''
          err: ''
        - command: [/testbin/opkg, install, zlib-dev]
          environ: *env-def
          rc: 0
          out: |
            Installing zlib-dev (1.2.11-6) to root...
            Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk
            Installing zlib (1.2.11-6) to root...
            Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib_1.2.11-6_mips_24kc.ipk
            Configuring zlib.
            Configuring zlib-dev.
          err: ''
        - command: [/testbin/opkg, list-installed, zlib-dev]
          environ: *env-def
          rc: 0
          out: |
            zlib-dev - 1.2.11-6
          err: ''
  - id: install_zlibdev_present
    input:
      name: zlib-dev
      state: present
    output:
      msg: package(s) already present
    mocks:
      run_command:
        - command: [/testbin/opkg, --version]
          environ: *env-def
          rc: 0
          out: ''
          err: ''
        - command: [/testbin/opkg, list-installed, zlib-dev]
          environ: *env-def
          rc: 0
          out: |
            zlib-dev - 1.2.11-6
          err: ''

Спецификации TestCaseMocks

Подкласс TestCaseMock может самостоятельно определять ожидаемую структуру данных.

Спецификация RunCommandMock

Имитируемые вызовы RunCommandMock можно указать с помощью ключа run_command; в качестве его значения ожидается list, элементы которого имеют следующую структуру:

  • command: Union[list, str]

    Обязательный. Команда, которую должен выполнить модуль. Соответствует параметру args вызова AnsibleModule.run_command(). Значением может быть список или строка, хотя обычно рекомендуется использовать список.

  • environ: dict

    Обязательный. Все остальные параметры, передаваемые вызову AnsibleModule.run_command(). Чаще всего используются environ_update и check_rc. Необходимо указать все параметры, используемые модулем Ansible при вызове AnsibleModule.run_command(), иначе тест завершится с ошибкой.

  • rc: int

    Обязательный. Код возврата команды. Как обычно в сценариях bash, значение 0 означает успех, а любое другое число — код ошибки.

  • out: str

    Обязательный. Результат выполнения команды в stdout — одна строка, содержащая ноль или более строк вывода.

  • err: str

    Обязательный. Результат выполнения команды в stderr — одна строка, содержащая ноль или более строк вывода.

Справочник по UTHelper

class .uthelper.UTHelper

Класс для инкапсуляции модульных тестов.

static from_spec(ansible_module, test_module, test_spec, mocks=None)

Создаёт экземпляр UTHelper на основе заданной спецификации тестов.

Параметры:
  • ansible_module (types.ModuleType) — Тестируемый модуль Ansible.
  • test_module (types.ModuleType) — Тестовый модуль.
  • test_spec (dict) — Спецификация тестов.
  • mocks (list или None) — Список объектов TestCaseMocks, используемых во время тестирования. В настоящее время доступен только RunCommandMock.
Возвращает:

Экземпляр UTHelper.

Тип возвращаемого значения:

UTHelper

Пример использования from_spec():

import sys

from ansible_collections.community.general.plugins.modules import ansible_module
from .uthelper import UTHelper, RunCommandMock

TEST_SPEC = dict(
    test_cases=[
        ...
    ]
)

helper = UTHelper.from_spec(ansible_module, sys.modules[__name__], TEST_SPEC, mocks=[RunCommandMock])
static from_file(ansible_module, test_module, test_spec_filehandle, mocks=None)

Создаёт экземпляр UTHelper на основе файла спецификации тестов.

Параметры:
  • ansible_module (types.ModuleType) — Тестируемый модуль Ansible.
  • test_module (types.ModuleType) — Тестовый модуль.
  • test_spec_filehandle (file-like object) — Дескриптор потока файла, содержащего спецификацию тестов в формате YAML.
  • mocks (list или None) — Список объектов TestCaseMocks, используемых во время тестирования. В настоящее время доступен только RunCommandMock.
Возвращает:

Экземпляр UTHelper.

Тип возвращаемого значения:

UTHelper

Пример использования from_file():

import sys

from ansible_collections.community.general.plugins.modules import ansible_module
from .uthelper import UTHelper, RunCommandMock

with open("test_spec.yaml", "r") as test_spec_filehandle:
    helper = UTHelper.from_file(ansible_module, sys.modules[__name__], test_spec_filehandle, mocks=[RunCommandMock])
static from_module(ansible_module, test_module_name, mocks=None)

Создаёт экземпляр UTHelper на основе заданных модуля Ansible и тестового модуля.

Параметры:
  • ansible_module (types.ModuleType) — Тестируемый модуль Ansible.
  • test_module_name (str) — Имя тестового модуля. В качестве значения можно передать __name__.
  • mocks (list или None) — Список объектов TestCaseMocks, используемых во время тестирования. В настоящее время доступен только RunCommandMock.
Возвращает:

Экземпляр UTHelper.

Тип возвращаемого значения:

UTHelper

Пример использования from_module():

from ansible_collections.community.general.plugins.modules import ansible_module
from .uthelper import UTHelper, RunCommandMock

# Example usage
helper = UTHelper.from_module(ansible_module, __name__, mocks=[RunCommandMock])

Создание TestCaseMocks

Чтобы создать новый TestCaseMock, необходимо расширить этот класс и реализовать соответствующие компоненты:

class ShrubberyMock(TestCaseMock):
    # this name is mandatory, it is the name used in the test specification
    name = "shrubbery"

    def setup(self, mocker):
        # perform setup, commonly using mocker to patch some other piece of code
        ...

    def check(self, test_case, results):
        # verify the tst execution met the expectations of the test case
        # for example the function was called as many times as it should
        ...

    def fixtures(self):
        # returns a dict mapping names to pytest fixtures that should be used for the test case
        # for example, in RunCommandMock it creates a fixture that patches AnsibleModule.get_bin_path
        ...

Ограничения

Известные проблемы и возможности для улучшения:

  • Только один UTHelper на тестовый модуль: UTHelper внедряет в пространство имён модуля тестовую функцию с фиксированным именем, поэтому добавление второго экземпляра UTHelper перезапишет функцию, созданную первым экземпляром.
  • В Python 3.5 порядок элементов в пространстве имён модуля не совпадает при разных запусках, поэтому при добавлении новых тестов Test Helper может добавить свою функцию до или после остальных тестовых функций. В коллекции community.general процессы CI используют pytest-xdist для параллельного выполнения и распределения тестов, для чего требуется, чтобы порядок тестов оставался неизменным.

Добавлено в версии 7.5.0.

© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/collections/community/general/docsite/guide_uthelper.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API