Skip to content

Commit 41a3c60

Browse files
authored
Contract test improvements (#52)
In this commit we are accomplishing two things: 1. We are making contract tests failures more robust by moving container instantiation into class methods so that if they fail, cleanup logic can be run (clean up logic moved from `tearDownClass` to `addClassCleanup`, which according to the docs means that it will actually run even if `setUpClass` fails). We also added try-catch blocks to ensure all logic attempts to run (e.g. if mock_collector fails to start, we still remove the network). These changes prevent things like lost containers/network buildup. 2. We are adding support for "dependency containers". Future contract tests depend on having another container running (e.g a dependency container), and adding the `set_up_dependency_container` and `tear_down_dependency_container` methods allow this to be done easily and correctly. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 0a5a4de commit 41a3c60

File tree

2 files changed

+70
-38
lines changed

2 files changed

+70
-38
lines changed

contract-tests/tests/test/amazon/base/contract_test_base.py

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,22 @@
66

77
from docker import DockerClient
88
from docker.models.networks import Network, NetworkCollection
9+
from docker.types import EndpointConfig
910
from mock_collector_client import MockCollectorClient
1011
from testcontainers.core.container import DockerContainer
1112
from testcontainers.core.waiting_utils import wait_for_logs
1213
from typing_extensions import override
1314

15+
NETWORK_NAME: str = "aws-appsignals-network"
16+
1417
_logger: Logger = getLogger(__name__)
1518
_logger.setLevel(INFO)
16-
1719
_MOCK_COLLECTOR_ALIAS: str = "collector"
1820
_MOCK_COLLECTOR_NAME: str = "aws-appsignals-mock-collector-python"
1921
_MOCK_COLLECTOR_PORT: int = 4315
20-
_NETWORK_NAME: str = "aws-appsignals-network"
21-
22-
_MOCK_COLLECTOR: DockerContainer = (
23-
DockerContainer(_MOCK_COLLECTOR_NAME).with_exposed_ports(_MOCK_COLLECTOR_PORT).with_name(_MOCK_COLLECTOR_NAME)
24-
)
25-
_NETWORK: Network = NetworkCollection(client=DockerClient()).create(_NETWORK_NAME)
2622

2723

24+
# pylint: disable=broad-exception-caught
2825
class ContractTestBase(TestCase):
2926
"""Base class for implementing a contract test.
3027
@@ -35,29 +32,53 @@ class ContractTestBase(TestCase):
3532
Several methods are provided that can be overridden to customize the test scenario.
3633
"""
3734

38-
_mock_collector_client: MockCollectorClient
39-
_application: DockerContainer
35+
application: DockerContainer
36+
mock_collector: DockerContainer
37+
mock_collector_client: MockCollectorClient
38+
network: Network
4039

4140
@classmethod
4241
@override
4342
def setUpClass(cls) -> None:
44-
_MOCK_COLLECTOR.start()
45-
wait_for_logs(_MOCK_COLLECTOR, "Ready", timeout=20)
46-
_NETWORK.connect(_MOCK_COLLECTOR_NAME, aliases=[_MOCK_COLLECTOR_ALIAS])
43+
cls.addClassCleanup(cls.class_tear_down)
44+
cls.network = NetworkCollection(client=DockerClient()).create(NETWORK_NAME)
45+
mock_collector_networking_config: Dict[str, EndpointConfig] = {
46+
NETWORK_NAME: EndpointConfig(version="1.22", aliases=[_MOCK_COLLECTOR_ALIAS])
47+
}
48+
cls.mock_collector: DockerContainer = (
49+
DockerContainer(_MOCK_COLLECTOR_NAME)
50+
.with_exposed_ports(_MOCK_COLLECTOR_PORT)
51+
.with_name(_MOCK_COLLECTOR_NAME)
52+
.with_kwargs(network=NETWORK_NAME, networking_config=mock_collector_networking_config)
53+
)
54+
cls.mock_collector.start()
55+
wait_for_logs(cls.mock_collector, "Ready", timeout=20)
56+
cls.set_up_dependency_container()
4757

4858
@classmethod
49-
@override
50-
def tearDownClass(cls) -> None:
51-
_logger.info("MockCollector stdout")
52-
_logger.info(_MOCK_COLLECTOR.get_logs()[0].decode())
53-
_logger.info("MockCollector stderr")
54-
_logger.info(_MOCK_COLLECTOR.get_logs()[1].decode())
55-
_MOCK_COLLECTOR.stop()
56-
_NETWORK.remove()
59+
def class_tear_down(cls) -> None:
60+
try:
61+
cls.tear_down_dependency_container()
62+
except Exception:
63+
_logger.exception("Failed to tear down dependency container")
64+
65+
try:
66+
_logger.info("MockCollector stdout")
67+
_logger.info(cls.mock_collector.get_logs()[0].decode())
68+
_logger.info("MockCollector stderr")
69+
_logger.info(cls.mock_collector.get_logs()[1].decode())
70+
cls.mock_collector.stop()
71+
except Exception:
72+
_logger.exception("Failed to tear down mock collector")
73+
74+
cls.network.remove()
5775

5876
@override
5977
def setUp(self) -> None:
60-
self._application: DockerContainer = (
78+
application_networking_config: Dict[str, EndpointConfig] = {
79+
NETWORK_NAME: EndpointConfig(version="1.22", aliases=self.get_application_network_aliases())
80+
}
81+
self.application: DockerContainer = (
6182
DockerContainer(self.get_application_image_name())
6283
.with_exposed_ports(self.get_application_port())
6384
.with_env("OTEL_METRIC_EXPORT_INTERVAL", "100")
@@ -68,31 +89,42 @@ def setUp(self) -> None:
6889
.with_env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", f"http://collector:{_MOCK_COLLECTOR_PORT}")
6990
.with_env("OTEL_RESOURCE_ATTRIBUTES", self.get_application_otel_resource_attributes())
7091
.with_env("OTEL_TRACES_SAMPLER", "always_on")
92+
.with_kwargs(network=NETWORK_NAME, networking_config=application_networking_config)
7193
.with_name(self.get_application_image_name())
7294
)
7395

7496
extra_env: Dict[str, str] = self.get_application_extra_environment_variables()
7597
for key in extra_env:
76-
self._application.with_env(key, extra_env.get(key))
77-
self._application.start()
78-
wait_for_logs(self._application, self.get_application_wait_pattern(), timeout=20)
79-
_NETWORK.connect(self.get_application_image_name(), aliases=self.get_application_network_aliases())
80-
81-
self._mock_collector_client: MockCollectorClient = MockCollectorClient(
82-
_MOCK_COLLECTOR.get_container_host_ip(), _MOCK_COLLECTOR.get_exposed_port(_MOCK_COLLECTOR_PORT)
98+
self.application.with_env(key, extra_env.get(key))
99+
self.application.start()
100+
wait_for_logs(self.application, self.get_application_wait_pattern(), timeout=20)
101+
self.mock_collector_client: MockCollectorClient = MockCollectorClient(
102+
self.mock_collector.get_container_host_ip(), self.mock_collector.get_exposed_port(_MOCK_COLLECTOR_PORT)
83103
)
84104

85105
@override
86106
def tearDown(self) -> None:
87-
_logger.info("Application stdout")
88-
_logger.info(self._application.get_logs()[0].decode())
89-
_logger.info("Application stderr")
90-
_logger.info(self._application.get_logs()[1].decode())
91-
self._application.stop()
92-
self._mock_collector_client.clear_signals()
107+
try:
108+
_logger.info("Application stdout")
109+
_logger.info(self.application.get_logs()[0].decode())
110+
_logger.info("Application stderr")
111+
_logger.info(self.application.get_logs()[1].decode())
112+
self.application.stop()
113+
except Exception:
114+
_logger.exception("Failed to tear down application")
115+
116+
self.mock_collector_client.clear_signals()
93117

94118
# pylint: disable=no-self-use
95119
# Methods that should be overridden in subclasses
120+
@classmethod
121+
def set_up_dependency_container(cls):
122+
return
123+
124+
@classmethod
125+
def tear_down_dependency_container(cls):
126+
return
127+
96128
def get_application_port(self) -> int:
97129
return 8080
98130

contract-tests/tests/test/amazon/requests/requests_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,18 @@ def test_fault_post(self) -> None:
6565
def do_test_requests(
6666
self, path: str, method: str, status_code: int, expected_error: int, expected_fault: int
6767
) -> None:
68-
address: str = self._application.get_container_host_ip()
69-
port: str = self._application.get_exposed_port(self.get_application_port())
68+
address: str = self.application.get_container_host_ip()
69+
port: str = self.application.get_exposed_port(self.get_application_port())
7070
url: str = f"http://{address}:{port}/{path}"
7171
response: Response = request(method, url, timeout=20)
7272

7373
self.assertEqual(status_code, response.status_code)
7474

75-
resource_scope_spans: List[ResourceScopeSpan] = self._mock_collector_client.get_traces()
75+
resource_scope_spans: List[ResourceScopeSpan] = self.mock_collector_client.get_traces()
7676
self._assert_aws_span_attributes(resource_scope_spans, method, path)
7777
self._assert_semantic_conventions_span_attributes(resource_scope_spans, method, path, status_code)
7878

79-
metrics: List[ResourceScopeMetric] = self._mock_collector_client.get_metrics(
79+
metrics: List[ResourceScopeMetric] = self.mock_collector_client.get_metrics(
8080
{LATENCY_METRIC, ERROR_METRIC, FAULT_METRIC}
8181
)
8282
self._assert_metric_attributes(metrics, method, path, LATENCY_METRIC, 5000)

0 commit comments

Comments
 (0)