From 95d2560ccef9bda2c45dc8a2b4cc7a7d8353574d Mon Sep 17 00:00:00 2001 From: Trung Thanh Phan Date: Fri, 5 Jan 2024 15:58:26 +0100 Subject: [PATCH] rework unit test to use pytest, add apt package install tests --- src/service.py | 2 +- tests/unit/conftest.py | 19 +++++++ tests/unit/test_charm.py | 110 ++++++++++++++++++------------------- tests/unit/test_service.py | 40 ++++++++++++++ 4 files changed, 114 insertions(+), 57 deletions(-) create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_service.py diff --git a/src/service.py b/src/service.py index a704bcf..25ed795 100644 --- a/src/service.py +++ b/src/service.py @@ -117,7 +117,7 @@ def install(self) -> None: apt.update() apt.add_package("openjdk-17-jre") apt.add_package(AGENT_PACKAGE_NAME) - except (apt.PackageError, apt.PackageNotFoundError) as exc: + except (apt.PackageError, apt.PackageNotFoundError, apt.GPGKeyError) as exc: raise PackageInstallError("Error installing the agent package") from exc def restart(self) -> None: diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..177e27b --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,19 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +"""Fixtures for jenkins-agent charm tests.""" + + +import pytest +from ops.testing import Harness + +from charm import JenkinsAgentCharm + + +@pytest.fixture(scope="function", name="harness") +def harness_fixture(): + """Enable ops test framework harness.""" + harness = Harness(JenkinsAgentCharm) + + yield harness + + harness.cleanup() diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 65225f0..81a8e6b 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -6,14 +6,14 @@ # pylint: disable=protected-access """Test for charm hooks.""" -import unittest -import unittest.mock -from unittest.mock import patch +from unittest.mock import MagicMock import ops import ops.testing +import pytest import charm_state +import service from charm import JenkinsAgentCharm @@ -29,58 +29,56 @@ def raise_exception(exception: Exception): raise exception -class TestCharm(unittest.TestCase): - """Test class for unit testing.""" +def test___init___invalid_state(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: patched State.from_charm that raises an InvalidState Error. + act: when the JenkinsAgentCharm is initialized. + assert: The agent falls into BlockedStatus. + """ + monkeypatch.setattr( + charm_state.State, + "from_charm", + MagicMock(side_effect=[charm_state.InvalidStateError("Invalid executor message")]), + ) - def setUp(self): - """Initialize the test class.""" - self.harness = ops.testing.Harness(JenkinsAgentCharm) - self.addCleanup(self.harness.cleanup) + harness.begin() - @patch( - "charm_state.State.from_charm", - side_effect=charm_state.InvalidStateError("Invalid executor message"), - ) - def test___init___invalid_state(self, _): - """ - arrange: patched State.from_charm that raises an InvalidState Error. - act: when the JenkinsAgentCharm is initialized. - assert: The agent falls into BlockedStatus. - """ - self.harness.begin() - - jenkins_charm: JenkinsAgentCharm = self.harness.charm - assert jenkins_charm.unit.status.name == ops.BlockedStatus.name - assert jenkins_charm.unit.status.message == "Invalid executor message" - - @patch("service.JenkinsAgentService.restart") - @patch("service.JenkinsAgentService.is_active", return_value=True) - @patch("ops.UpgradeCharmEvent") - def test__on_upgrade_charm(self, _service_restart, _service_is_active, _upgrade_charm_event): - """ - arrange: given a charm with patched agent service that is active. - act: when _on_upgrade_charm is called. - assert: The agent falls into waiting status with the correct message. - """ - self.harness.begin() - - jenkins_charm: JenkinsAgentCharm = self.harness.charm - jenkins_charm._on_upgrade_charm(_upgrade_charm_event) - - assert jenkins_charm.unit.status.message == "Waiting for relation." - assert jenkins_charm.unit.status.name == ops.BlockedStatus.name - - @patch("ops.ConfigChangedEvent") - @patch("ops.Model.get_relation") - def test__on_config_changed(self, _get_relation_mock, _config_changed_event): - """ - arrange: given a charm with patched relation. - act: when _on_config_changed is called. - assert: The charm correctly updates the relation databag. - """ - self.harness.begin() - jenkins_charm: JenkinsAgentCharm = self.harness.charm - jenkins_charm._on_config_changed(_config_changed_event) - - agent_relation = _get_relation_mock.return_value - assert agent_relation.data[self.harness._unit_name].update.call_count == 1 + jenkins_charm: JenkinsAgentCharm = harness.charm + assert jenkins_charm.unit.status.name == ops.BlockedStatus.name + assert jenkins_charm.unit.status.message == "Invalid executor message" + + +def test__on_upgrade_charm(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: given a charm with patched agent service that is active. + act: when _on_upgrade_charm is called. + assert: The agent falls into waiting status with the correct message. + """ + monkeypatch.setattr(service.JenkinsAgentService, "is_active", MagicMock(return_value=True)) + monkeypatch.setattr(service.JenkinsAgentService, "restart", MagicMock()) + harness.begin() + + jenkins_charm: JenkinsAgentCharm = harness.charm + upgrade_charm_event = MagicMock(spec=ops.UpgradeCharmEvent) + jenkins_charm._on_upgrade_charm(upgrade_charm_event) + + assert jenkins_charm.unit.status.message == "Waiting for relation." + assert jenkins_charm.unit.status.name == ops.BlockedStatus.name + + +def test__on_config_changed(harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch): + """ + arrange: given a charm with patched relation. + act: when _on_config_changed is called. + assert: The charm correctly updates the relation databag. + """ + harness.begin() + config_changed_event = MagicMock(spec=ops.ConfigChangedEvent) + get_relation_mock = MagicMock() + monkeypatch.setattr(ops.Model, "get_relation", get_relation_mock) + + jenkins_charm: JenkinsAgentCharm = harness.charm + jenkins_charm._on_config_changed(config_changed_event) + + agent_relation = get_relation_mock.return_value + assert agent_relation.data[harness._unit_name].update.call_count == 1 diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py new file mode 100644 index 0000000..3d366ca --- /dev/null +++ b/tests/unit/test_service.py @@ -0,0 +1,40 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +"""Test for service interaction.""" +# pylint: disable=protected-access +from unittest.mock import MagicMock + +import ops.testing +import pytest +from charms.operator_libs_linux.v0 import apt + +from charm import JenkinsAgentCharm + + +@pytest.mark.parametrize( + "f,error_thrown", + [ + ("import_key", apt.GPGKeyError), + ("add_package", apt.PackageError), + ("add_package", apt.PackageNotFoundError), + ], +) +def test_install_apt_package_gpg_key_error( + harness: ops.testing.Harness, monkeypatch: pytest.MonkeyPatch, f, error_thrown +): + """ + arrange: Harness with mocked apt module. + act: run _on_install hook with methods raising different errors. + assert: The charm should be in an error state. + """ + harness.begin() + charm: JenkinsAgentCharm = harness.charm + monkeypatch.setattr(apt, "RepositoryMapping", MagicMock()) + monkeypatch.setattr(apt, "import_key", MagicMock()) + monkeypatch.setattr(apt, "update", MagicMock()) + monkeypatch.setattr(apt, "add_package", MagicMock()) + + monkeypatch.setattr(apt, f, MagicMock(side_effect=[error_thrown])) + + with pytest.raises(RuntimeError, match="Error installing the agent service"): + charm._on_install(MagicMock(spec=ops.InstallEvent))