Руководство по 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.
-
ansible_module (
- Возвращает:
-
Экземпляр
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.
-
ansible_module (
- Возвращает:
-
Экземпляр
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.
-
ansible_module (
- Возвращает:
-
Экземпляр
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