From 9848f660367b27ce3463c8a102c54a23db3a356e Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Fri, 12 Apr 2024 09:26:49 +0200 Subject: [PATCH 1/3] cleanup: Migrate existing unit tests to pytest Cleanup the existing unit tests and move them to pytest. By using pytest it becomes easier to add new tests for existing functionality in the collection. Signed-off-by: Felix Matouschek --- test-requirements.txt | 1 + tests/unit/conftest.py | 44 --- tests/unit/modules/test_module_kubevirt_vm.py | 285 ++++++++---------- .../modules/test_module_kubevirt_vm_info.py | 66 ++-- tests/unit/utils/ansible_module_mock.py | 10 +- 5 files changed, 159 insertions(+), 247 deletions(-) delete mode 100644 tests/unit/conftest.py diff --git a/test-requirements.txt b/test-requirements.txt index f90b181..8784bed 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,5 @@ pytest pytest-ansible +pytest-mock pytest-xdist tox-ansible diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py deleted file mode 100644 index 20615ad..0000000 --- a/tests/unit/conftest.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import json -import sys -from io import BytesIO - -import pytest - -import ansible.module_utils.basic -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_bytes -from ansible.module_utils.common._collections_compat import MutableMapping - - -@pytest.fixture -def stdin(mocker, request): - old_args = ansible.module_utils.basic._ANSIBLE_ARGS - ansible.module_utils.basic._ANSIBLE_ARGS = None - old_argv = sys.argv - sys.argv = ["ansible_unittest"] - - if isinstance(request.param, string_types): - args = request.param - elif isinstance(request.param, MutableMapping): - if "ANSIBLE_MODULE_ARGS" not in request.param: - request.param = {"ANSIBLE_MODULE_ARGS": request.param} - if "_ansible_remote_tmp" not in request.param["ANSIBLE_MODULE_ARGS"]: - request.param["ANSIBLE_MODULE_ARGS"]["_ansible_remote_tmp"] = "/tmp" - if "_ansible_keep_remote_files" not in request.param["ANSIBLE_MODULE_ARGS"]: - request.param["ANSIBLE_MODULE_ARGS"]["_ansible_keep_remote_files"] = False - args = json.dumps(request.param) - else: - raise Exception("Malformed data to the stdin pytest fixture") - - fake_stdin = BytesIO(to_bytes(args, errors="surrogate_or_strict")) - mocker.patch("ansible.module_utils.basic.sys.stdin", mocker.MagicMock()) - mocker.patch("ansible.module_utils.basic.sys.stdin.buffer", fake_stdin) - - yield fake_stdin - - ansible.module_utils.basic._ANSIBLE_ARGS = old_args - sys.argv = old_argv diff --git a/tests/unit/modules/test_module_kubevirt_vm.py b/tests/unit/modules/test_module_kubevirt_vm.py index 16f5964..ee78b69 100644 --- a/tests/unit/modules/test_module_kubevirt_vm.py +++ b/tests/unit/modules/test_module_kubevirt_vm.py @@ -6,11 +6,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -import unittest +import pytest -from unittest.mock import patch, ANY - -from ansible.module_utils import basic +from ansible.module_utils.basic import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.k8s import runner from ansible_collections.kubevirt.core.plugins.modules import kubevirt_vm from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import ( @@ -19,47 +17,55 @@ from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock impo exit_json, fail_json, set_module_args, - get_api_client, ) -FIXTURE1 = { - "apiVersion": "kubevirt.io/v1", - "kind": "VirtualMachine", - "metadata": { - "name": "testvm", - "namespace": "default", - "labels": {"environment": "staging", "service": "loadbalancer"}, - }, - "spec": { - "running": True, - "instancetype": {"name": "u1.medium"}, - "preference": {"name": "fedora"}, - "dataVolumeTemplates": [ - { - "metadata": {"name": "testdv"}, - "spec": { - "source": { - "registry": { - "url": "docker://quay.io/containerdisks/fedora:latest" + +@pytest.fixture(scope="module") +def vm_definition(): + return { + "apiVersion": "kubevirt.io/v1", + "kind": "VirtualMachine", + "metadata": { + "name": "testvm", + "namespace": "default", + "labels": {"environment": "staging", "service": "loadbalancer"}, + }, + "spec": { + "running": True, + "instancetype": {"name": "u1.medium"}, + "preference": {"name": "fedora"}, + "dataVolumeTemplates": [ + { + "metadata": {"name": "testdv"}, + "spec": { + "source": { + "registry": { + "url": "docker://quay.io/containerdisks/fedora:latest" + }, + }, + "storage": { + "accessModes": ["ReadWriteOnce"], + "resources": {"requests": {"storage": "5Gi"}}, }, }, - "storage": { - "accessModes": ["ReadWriteOnce"], - "resources": {"requests": {"storage": "5Gi"}}, - }, + } + ], + "template": { + "metadata": { + "labels": {"environment": "staging", "service": "loadbalancer"} + }, + "spec": { + "domain": {"devices": {}}, + "terminationGracePeriodSeconds": 180, }, - } - ], - "template": { - "metadata": { - "labels": {"environment": "staging", "service": "loadbalancer"} }, - "spec": {"domain": {"devices": {}}, "terminationGracePeriodSeconds": 180}, }, - }, -} + } -METADATA = """apiVersion: kubevirt.io/v1 + +@pytest.fixture(scope="module") +def vm_manifest(): + return """apiVersion: kubevirt.io/v1 kind: VirtualMachine metadata: name: "testvm" @@ -97,121 +103,96 @@ spec: terminationGracePeriodSeconds: 180 """ -FIXTURE2 = { - "name": "testvm", - "namespace": "default", - "state": "present", - "labels": {"service": "loadbalancer", "environment": "staging"}, - "instancetype": {"name": "u1.medium"}, - "preference": {"name": "fedora"}, - "data_volume_templates": [ - { - "metadata": {"name": "testdv"}, - "spec": { - "source": { - "registry": { - "url": "docker://quay.io/containerdisks/fedora:latest" + +@pytest.fixture(scope="module") +def module_params_create(): + return { + "name": "testvm", + "namespace": "default", + "state": "present", + "labels": {"service": "loadbalancer", "environment": "staging"}, + "instancetype": {"name": "u1.medium"}, + "preference": {"name": "fedora"}, + "data_volume_templates": [ + { + "metadata": {"name": "testdv"}, + "spec": { + "source": { + "registry": { + "url": "docker://quay.io/containerdisks/fedora:latest" + }, + }, + "storage": { + "accessModes": ["ReadWriteOnce"], + "resources": {"requests": {"storage": "5Gi"}}, }, }, - "storage": { - "accessModes": ["ReadWriteOnce"], - "resources": {"requests": {"storage": "5Gi"}}, - }, - }, - } - ], - "spec": {"domain": {"devices": {}}, "terminationGracePeriodSeconds": 180}, - "api_version": "kubevirt.io/v1", - "running": True, - "wait": False, - "wait_sleep": 5, - "wait_timeout": 120, - "force": False, - "generate_name": None, - "annotations": None, - "kubeconfig": None, - "context": None, - "host": None, - "api_key": None, - "username": None, - "password": None, - "validate_certs": None, - "ca_cert": None, - "client_cert": None, - "client_key": None, - "proxy": None, - "no_proxy": None, - "proxy_headers": None, - "persist_config": None, - "impersonate_user": None, - "impersonate_groups": None, - "delete_options": None, - "resource_definition": METADATA, - "wait_condition": {"type": "Ready", "status": True}, -} - - -class TestCreateVM(unittest.TestCase): - def setUp(self): - self.mock_module_helper = patch.multiple( - basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json - ) - self.mock_module_helper.start() - - self.mock_runner = patch.multiple(runner, get_api_client=get_api_client) - self.mock_runner.start() - - # Stop the patch after test execution - # like tearDown but executed also when the setup failed - self.addCleanup(self.mock_module_helper.stop) - self.addCleanup(self.mock_runner.stop) - - def test_module_fail_when_required_args_missing(self): - with self.assertRaises(AnsibleFailJson): - set_module_args({}) - kubevirt_vm.main() - - def test_create(self): - set_module_args( - { - "name": "testvm", - "namespace": "default", - "state": "present", - "labels": {"service": "loadbalancer", "environment": "staging"}, - "instancetype": {"name": "u1.medium"}, - "preference": {"name": "fedora"}, - "data_volume_templates": [ - { - "metadata": {"name": "testdv"}, - "spec": { - "source": { - "registry": { - "url": "docker://quay.io/containerdisks/fedora:latest" - }, - }, - "storage": { - "accessModes": ["ReadWriteOnce"], - "resources": {"requests": {"storage": "5Gi"}}, - }, - }, - } - ], - "spec": { - "domain": {"devices": {}}, - "terminationGracePeriodSeconds": 180, - }, } - ) - with patch.object(runner, "perform_action") as mock_run_command: - mock_run_command.return_value = { - "method": "create", - "changed": True, - "result": "success", - } # successful execution - with self.assertRaises(AnsibleExitJson) as result: - kubevirt_vm.main() - mock_run_command.assert_called_once_with( - ANY, - FIXTURE1, - FIXTURE2, - ) + ], + "spec": { + "domain": {"devices": {}}, + "terminationGracePeriodSeconds": 180, + }, + } + + +@pytest.fixture(scope="module") +def k8s_module_params_create(module_params_create, vm_manifest): + return module_params_create | { + "api_version": "kubevirt.io/v1", + "running": True, + "wait": False, + "wait_sleep": 5, + "wait_timeout": 120, + "force": False, + "generate_name": None, + "annotations": None, + "kubeconfig": None, + "context": None, + "host": None, + "api_key": None, + "username": None, + "password": None, + "validate_certs": None, + "ca_cert": None, + "client_cert": None, + "client_key": None, + "proxy": None, + "no_proxy": None, + "proxy_headers": None, + "persist_config": None, + "impersonate_user": None, + "impersonate_groups": None, + "delete_options": None, + "resource_definition": vm_manifest, + "wait_condition": {"type": "Ready", "status": True}, + } + + +def test_module_fails_when_required_args_missing(monkeypatch): + monkeypatch.setattr(AnsibleModule, "fail_json", fail_json) + with pytest.raises(AnsibleFailJson): + set_module_args({}) + kubevirt_vm.main() + + +def test_module_create( + monkeypatch, mocker, module_params_create, k8s_module_params_create, vm_definition +): + monkeypatch.setattr(AnsibleModule, "exit_json", exit_json) + monkeypatch.setattr(runner, "get_api_client", lambda _: None) + + set_module_args(module_params_create) + + perform_action = mocker.patch.object(runner, "perform_action") + perform_action.return_value = { + "method": "create", + "changed": True, + "result": "success", + } + + with pytest.raises(AnsibleExitJson): + kubevirt_vm.main() + perform_action.assert_called_once_with( + mocker.ANY, vm_definition, k8s_module_params_create + ) diff --git a/tests/unit/modules/test_module_kubevirt_vm_info.py b/tests/unit/modules/test_module_kubevirt_vm_info.py index 67dc279..f06d0bd 100644 --- a/tests/unit/modules/test_module_kubevirt_vm_info.py +++ b/tests/unit/modules/test_module_kubevirt_vm_info.py @@ -6,8 +6,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from unittest import TestCase -from unittest.mock import patch +import pytest from ansible.module_utils.basic import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.k8s.service import ( @@ -19,12 +18,10 @@ from ansible_collections.kubevirt.core.plugins.modules import ( from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import ( AnsibleExitJson, exit_json, - fail_json, set_module_args, - get_api_client, ) -FIXTURE1 = { +FIND_ARGS_DEFAULT = { "kind": "VirtualMachine", "api_version": "kubevirt.io/v1", "name": None, @@ -37,7 +34,7 @@ FIXTURE1 = { "condition": {"type": "Ready", "status": True}, } -FIXTURE2 = { +FIND_ARGS_NAME_NAMESPACE = { "kind": "VirtualMachine", "api_version": "kubevirt.io/v1", "name": "testvm", @@ -51,43 +48,26 @@ FIXTURE2 = { } -class TestDescribeVM(TestCase): - def setUp(self): - self.mock_module_helper = patch.multiple( - AnsibleModule, exit_json=exit_json, fail_json=fail_json - ) - self.mock_module_helper.start() +@pytest.mark.parametrize( + "module_args,find_args", + [ + ({}, FIND_ARGS_DEFAULT), + ({"name": "testvm", "namespace": "default"}, FIND_ARGS_NAME_NAMESPACE), + ], +) +def test_module(monkeypatch, mocker, module_args, find_args): + monkeypatch.setattr(AnsibleModule, "exit_json", exit_json) + monkeypatch.setattr(kubevirt_vm_info, "get_api_client", lambda _: None) - self.mock_main = patch.multiple(kubevirt_vm_info, get_api_client=get_api_client) - self.mock_main.start() + set_module_args(module_args) - # Stop the patch after test execution - # like tearDown but executed also when the setup failed - self.addCleanup(self.mock_module_helper.stop) - self.addCleanup(self.mock_main.stop) + find = mocker.patch.object(K8sService, "find") + find.return_value = { + "api_found": True, + "failed": False, + "resources": [], + } - def run_module(self, fixture): - with patch.object(K8sService, "find") as mock_find_command: - mock_find_command.return_value = { - "api_found": True, - "failed": False, - "resources": [], - } # successful execution - with self.assertRaises(AnsibleExitJson): - kubevirt_vm_info.main() - mock_find_command.assert_called_once_with( - **fixture, - ) - - def test_describe_without_args(self): - set_module_args({}) - self.run_module(FIXTURE1) - - def test_describe_with_args(self): - set_module_args( - { - "name": "testvm", - "namespace": "default", - } - ) - self.run_module(FIXTURE2) + with pytest.raises(AnsibleExitJson): + kubevirt_vm_info.main() + find.assert_called_once_with(**find_args) diff --git a/tests/unit/utils/ansible_module_mock.py b/tests/unit/utils/ansible_module_mock.py index 7904f9f..d489756 100644 --- a/tests/unit/utils/ansible_module_mock.py +++ b/tests/unit/utils/ansible_module_mock.py @@ -2,7 +2,8 @@ # Copyright: (c) 2021, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# This module maock the AnsibleModule class for more information please visite +# This file allows to run modules in unit tests. +# It was taken from: # https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html#module-argument-processing from __future__ import absolute_import, division, print_function @@ -23,13 +24,11 @@ def set_module_args(args): class AnsibleExitJson(Exception): """Exception class to be raised by module.exit_json and caught by the test case""" - pass class AnsibleFailJson(Exception): """Exception class to be raised by module.fail_json and caught by the test case""" - pass @@ -44,8 +43,3 @@ def fail_json(*args, **kwargs): """function to patch over fail_json; package return data into an exception""" kwargs["failed"] = True raise AnsibleFailJson(kwargs) - - -def get_api_client(*args, **kwargs): - """function to patch over get_api_client""" - pass From 1947fe9ca89eb0b20ade919e50610c8c67c27493 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Fri, 12 Apr 2024 09:32:40 +0200 Subject: [PATCH 2/3] cleanup: Reorganize unit tests Reorganize unit tests so the directory structure looks like the structure used in most other collections. Signed-off-by: Felix Matouschek --- .../modules/test_kubevirt_vm.py} | 0 .../modules/test_kubevirt_vm_info.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{modules/test_module_kubevirt_vm.py => plugins/modules/test_kubevirt_vm.py} (100%) rename tests/unit/{modules/test_module_kubevirt_vm_info.py => plugins/modules/test_kubevirt_vm_info.py} (100%) diff --git a/tests/unit/modules/test_module_kubevirt_vm.py b/tests/unit/plugins/modules/test_kubevirt_vm.py similarity index 100% rename from tests/unit/modules/test_module_kubevirt_vm.py rename to tests/unit/plugins/modules/test_kubevirt_vm.py diff --git a/tests/unit/modules/test_module_kubevirt_vm_info.py b/tests/unit/plugins/modules/test_kubevirt_vm_info.py similarity index 100% rename from tests/unit/modules/test_module_kubevirt_vm_info.py rename to tests/unit/plugins/modules/test_kubevirt_vm_info.py From 5ae19f96235b6230e237b9142ea3d2d248046c48 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Fri, 12 Apr 2024 09:55:20 +0200 Subject: [PATCH 3/3] tests: Add basic unit tests for inventory plugin Add a test file for the inventory plugin with some basic unit tests. This file can be used to add more tests in the future. Signed-off-by: Felix Matouschek --- tests/unit/plugins/inventory/test_kubevirt.py | 52 ++++++++++++++++ .../plugins/modules/test_kubevirt_vm_info.py | 62 ++++++++++--------- 2 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 tests/unit/plugins/inventory/test_kubevirt.py diff --git a/tests/unit/plugins/inventory/test_kubevirt.py b/tests/unit/plugins/inventory/test_kubevirt.py new file mode 100644 index 0000000..bf635b1 --- /dev/null +++ b/tests/unit/plugins/inventory/test_kubevirt.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat, Inc. +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest + +from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import InventoryModule + + +@pytest.fixture +def inventory(): + return InventoryModule() + + +@pytest.mark.parametrize( + "file_name,expected", + [ + ("inventory.kubevirt.yml", True), + ("inventory.kubevirt.yaml", True), + ("something.kubevirt.yml", True), + ("something.kubevirt.yaml", True), + ("inventory.somethingelse.yml", False), + ("inventory.somethingelse.yaml", False), + ("something.somethingelse.yml", False), + ("something.somethingelse.yaml", False), + ], +) +def test_verify_file(tmp_path, inventory, file_name, expected): + file = tmp_path / file_name + file.touch() + assert inventory.verify_file(str(file)) is expected + + +@pytest.mark.parametrize( + "file_name", + [ + "inventory.kubevirt.yml", + "inventory.kubevirt.yaml", + "something.kubevirt.yml", + "something.kubevirt.yaml", + "inventory.somethingelse.yml", + "inventory.somethingelse.yaml", + "something.somethingelse.yml", + "something.somethingelse.yaml", + ], +) +def test_verify_file_bad_config(inventory, file_name): + assert inventory.verify_file(file_name) is False diff --git a/tests/unit/plugins/modules/test_kubevirt_vm_info.py b/tests/unit/plugins/modules/test_kubevirt_vm_info.py index f06d0bd..41e26cf 100644 --- a/tests/unit/plugins/modules/test_kubevirt_vm_info.py +++ b/tests/unit/plugins/modules/test_kubevirt_vm_info.py @@ -21,41 +21,47 @@ from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock impo set_module_args, ) -FIND_ARGS_DEFAULT = { - "kind": "VirtualMachine", - "api_version": "kubevirt.io/v1", - "name": None, - "namespace": None, - "label_selectors": [], - "field_selectors": [], - "wait": False, - "wait_sleep": 5, - "wait_timeout": 120, - "condition": {"type": "Ready", "status": True}, -} -FIND_ARGS_NAME_NAMESPACE = { - "kind": "VirtualMachine", - "api_version": "kubevirt.io/v1", - "name": "testvm", - "namespace": "default", - "label_selectors": [], - "field_selectors": [], - "wait": False, - "wait_sleep": 5, - "wait_timeout": 120, - "condition": {"type": "Ready", "status": True}, -} +@pytest.fixture(scope="module") +def find_args_default(): + return { + "kind": "VirtualMachine", + "api_version": "kubevirt.io/v1", + "name": None, + "namespace": None, + "label_selectors": [], + "field_selectors": [], + "wait": False, + "wait_sleep": 5, + "wait_timeout": 120, + "condition": {"type": "Ready", "status": True}, + } + + +@pytest.fixture(scope="module") +def find_args_name_namespace(): + return { + "kind": "VirtualMachine", + "api_version": "kubevirt.io/v1", + "name": "testvm", + "namespace": "default", + "label_selectors": [], + "field_selectors": [], + "wait": False, + "wait_sleep": 5, + "wait_timeout": 120, + "condition": {"type": "Ready", "status": True}, + } @pytest.mark.parametrize( "module_args,find_args", [ - ({}, FIND_ARGS_DEFAULT), - ({"name": "testvm", "namespace": "default"}, FIND_ARGS_NAME_NAMESPACE), + ({}, "find_args_default"), + ({"name": "testvm", "namespace": "default"}, "find_args_name_namespace"), ], ) -def test_module(monkeypatch, mocker, module_args, find_args): +def test_module(request, monkeypatch, mocker, module_args, find_args): monkeypatch.setattr(AnsibleModule, "exit_json", exit_json) monkeypatch.setattr(kubevirt_vm_info, "get_api_client", lambda _: None) @@ -70,4 +76,4 @@ def test_module(monkeypatch, mocker, module_args, find_args): with pytest.raises(AnsibleExitJson): kubevirt_vm_info.main() - find.assert_called_once_with(**find_args) + find.assert_called_once_with(**request.getfixturevalue(find_args))