Skip to content

Commit 2eeb31d

Browse files
authored
[ISV-5275] Prepare the integration test framework scaffolding (2/2) (#754)
* [ISV-5275] Prepare the integration test framework scaffolding (2/2) Signed-off-by: Maurizio Porrato <mporrato@redhat.com>
1 parent 3b4e511 commit 2eeb31d

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Framework for the definition of integration test cases as python classes.
3+
"""
4+
5+
import logging
6+
from typing import TypeVar
7+
8+
from operatorcert.integration.config import Config
9+
from colorama import Fore, Style, init as colorama_init
10+
11+
12+
LOGGER = logging.getLogger("operator-cert")
13+
14+
15+
class BaseTestCase:
16+
"""
17+
The base class for user-defined integration test cases
18+
"""
19+
20+
def __init__(self, config: Config, logger: logging.Logger) -> None:
21+
self.config = config
22+
self.logger = logger
23+
24+
def setup(self) -> None:
25+
"""
26+
This method is the first to be called in the test execution and should
27+
create the resources required by the test case
28+
"""
29+
30+
def watch(self) -> None:
31+
"""
32+
This method is called after `setup()` and should watch the execution
33+
of the pipeline and either return when the pipeline has finished or
34+
raise an exception if it reaches an unexpected state
35+
"""
36+
37+
def validate(self) -> None:
38+
"""
39+
This method is called after `watch()` terminates and should check
40+
the state of all the resources involved in the test case and raise
41+
an exception if the actual state does not match the expected state
42+
"""
43+
44+
def cleanup(self) -> None:
45+
"""
46+
This method is called at the very end of the test case, even if a
47+
previous step raised an exception; it should be used to free up
48+
any resources created during the execution of the test
49+
"""
50+
51+
def run(self) -> None:
52+
"""
53+
Execute the test case; `setup()`, `watch()`, `validate()` and
54+
`cleanup()` are called in order
55+
"""
56+
try:
57+
self.setup()
58+
self.watch()
59+
self.validate()
60+
except Exception as e:
61+
raise e
62+
finally:
63+
self.cleanup()
64+
65+
66+
_test_cases = []
67+
68+
69+
_T = TypeVar("_T", bound=type)
70+
71+
72+
def integration_test_case(test_class: _T) -> _T:
73+
"""
74+
Decorator used to register a class as a test case
75+
"""
76+
_test_cases.append(test_class)
77+
return test_class
78+
79+
80+
def run_tests(config: Config) -> int:
81+
"""
82+
Executes all the test cases that have been registered using the
83+
`integration_test_case` decorator
84+
85+
Return:
86+
number of test cases that failed
87+
"""
88+
colorama_init()
89+
failed = 0
90+
for test_class in _test_cases:
91+
test_name = test_class.__name__
92+
print(f"Running {test_name} ", end="")
93+
try:
94+
test_instance = test_class(config, LOGGER)
95+
test_instance.run()
96+
print(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
97+
except Exception as e: # pylint: disable=broad-except
98+
print(f"{Fore.RED}FAIL{Style.RESET_ALL}")
99+
LOGGER.error("Test %s failed:", test_name, exc_info=e)
100+
failed += 1
101+
return failed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from unittest.mock import patch, MagicMock
2+
3+
import pytest
4+
5+
from operatorcert.integration.config import Config
6+
from operatorcert.integration.testcase import (
7+
BaseTestCase,
8+
integration_test_case,
9+
run_tests,
10+
)
11+
12+
13+
@patch("operatorcert.integration.testcase.BaseTestCase.setup")
14+
@patch("operatorcert.integration.testcase.BaseTestCase.watch")
15+
@patch("operatorcert.integration.testcase.BaseTestCase.validate")
16+
@patch("operatorcert.integration.testcase.BaseTestCase.cleanup")
17+
def test_basetestcase(
18+
mock_cleanup: MagicMock,
19+
mock_validate: MagicMock,
20+
mock_watch: MagicMock,
21+
mock_setup: MagicMock,
22+
) -> None:
23+
t = BaseTestCase(MagicMock(), MagicMock())
24+
25+
# happy path
26+
t.run()
27+
mock_setup.assert_called_once()
28+
mock_watch.assert_called_once()
29+
mock_validate.assert_called_once()
30+
mock_cleanup.assert_called_once()
31+
32+
# test raises exception
33+
for m in (mock_setup, mock_watch, mock_validate, mock_cleanup):
34+
m.reset_mock()
35+
36+
mock_watch.side_effect = Exception()
37+
with pytest.raises(Exception):
38+
t.run()
39+
mock_setup.assert_called_once()
40+
mock_watch.assert_called_once()
41+
mock_validate.assert_not_called()
42+
mock_cleanup.assert_called_once()
43+
44+
45+
@patch("operatorcert.integration.testcase._test_cases")
46+
def test_integration_test_case(mock_test_cases: MagicMock) -> None:
47+
fake_class = MagicMock(spec=BaseTestCase)
48+
integration_test_case(fake_class)
49+
mock_test_cases.append.assert_called_once_with(fake_class)
50+
51+
52+
class GoodTest(BaseTestCase):
53+
pass
54+
55+
56+
class BadTest(BaseTestCase):
57+
def watch(self) -> None:
58+
raise Exception()
59+
60+
61+
@patch("operatorcert.integration.testcase._test_cases", [GoodTest, BadTest, GoodTest])
62+
def test_run() -> None:
63+
fake_config = MagicMock(spec=Config)
64+
assert run_tests(fake_config) == 1

pdm.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ operatorcert-dev = [
101101
"jmespath>=1.0.1",
102102
"kubernetes>=31.0.0",
103103
"ansible-lint>=24.10.0",
104+
"colorama>=0.4.6",
104105
]
105106
tox = ["tox>=4.16.0", "tox-pdm>=0.7.2"]
106107

0 commit comments

Comments
 (0)