Skip to content

Commit abe894c

Browse files
authored
Clean up patch tests (#212)
Follow up from #204, which had to be discarded, but changes to `test_instrumentation_patch.py` still have value. Class had to be significantly re-written for reasons that are made clear in the class-level comments. 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 f838f5d commit abe894c

File tree

1 file changed

+79
-38
lines changed

1 file changed

+79
-38
lines changed

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_instrumentation_patch.py

Lines changed: 79 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,71 @@
1515
_QUEUE_NAME: str = "queueName"
1616
_QUEUE_URL: str = "queueUrl"
1717

18+
# Patch names
19+
GET_DISTRIBUTION_PATCH: str = (
20+
"amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution"
21+
)
1822

19-
class TestInstrumentationPatch(TestCase):
20-
@classmethod
21-
def setUpClass(cls):
22-
super().setUpClass()
23-
cls.mock_get_distribution = patch(
24-
"amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution"
25-
).start()
26-
27-
@classmethod
28-
def tearDownClass(cls):
29-
super().tearDownClass()
30-
cls.mock_get_distribution.stop()
31-
32-
def test_botocore_not_installed(self):
33-
# Test scenario 1: Botocore package not installed
34-
self.mock_get_distribution.side_effect = pkg_resources.DistributionNotFound
35-
apply_instrumentation_patches()
36-
with patch(
37-
"amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
38-
) as mock_apply_patches:
39-
mock_apply_patches.assert_not_called()
4023

41-
def test_botocore_installed_wrong_version(self):
42-
# Test scenario 2: Botocore package installed with wrong version
43-
self.mock_get_distribution.side_effect = pkg_resources.VersionConflict("botocore==1.0.0", "botocore==0.0.1")
44-
apply_instrumentation_patches()
45-
with patch(
46-
"amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
47-
) as mock_apply_patches:
48-
mock_apply_patches.assert_not_called()
24+
class TestInstrumentationPatch(TestCase):
25+
"""
26+
This test class has exactly one test, test_instrumentation_patch. This is an anti-pattern, but the scenario is
27+
fairly unusual and we feel justifies the code smell. Essentially the _instrumentation_patch module monkey-patches
28+
upstream components, so once it's run, it's challenging to "undo" between tests. To work around this, we have a
29+
monolith test framework that tests two major categories of test scenarios:
30+
1. Patch behaviour
31+
2. Patch mechanism
32+
33+
Patch behaviour tests validate upstream behaviour without patches, apply patches, and validate patched behaviour.
34+
Patch mechanism tests validate the logic that is used to actually apply patches, and can be run regardless of the
35+
pre- or post-patch behaviour.
36+
"""
37+
38+
method_patches: Dict[str, patch] = {}
39+
mock_metric_exporter_init: patch
40+
41+
def test_instrumentation_patch(self):
42+
# Set up method patches used by all tests
43+
self.method_patches[GET_DISTRIBUTION_PATCH] = patch(GET_DISTRIBUTION_PATCH).start()
44+
45+
# Run tests that validate patch behaviour before and after patching
46+
self._run_patch_behaviour_tests()
47+
# Run tests not specifically related to patch behaviour
48+
self._run_patch_mechanism_tests()
49+
50+
# Clean up method patches
51+
for method_patch in self.method_patches.values():
52+
method_patch.stop()
53+
54+
def _run_patch_behaviour_tests(self):
55+
# Test setup
56+
self.method_patches[GET_DISTRIBUTION_PATCH].return_value = "CorrectDistributionObject"
4957

50-
def test_botocore_installed_correct_version(self):
51-
# Test scenario 3: Botocore package installed with correct version
5258
# Validate unpatched upstream behaviour - important to detect upstream changes that may break instrumentation
53-
self._validate_unpatched_botocore_instrumentation()
54-
55-
self.mock_get_distribution.return_value = "CorrectDistributionObject"
59+
self._test_unpatched_botocore_instrumentation()
5660

5761
# Apply patches
5862
apply_instrumentation_patches()
5963

6064
# Validate patched upstream behaviour - important to detect downstream changes that may break instrumentation
61-
self._validate_patched_botocore_instrumentation()
62-
63-
def _validate_unpatched_botocore_instrumentation(self):
65+
self._test_patched_botocore_instrumentation()
66+
67+
# Test teardown
68+
self._reset_mocks()
69+
70+
def _run_patch_mechanism_tests(self):
71+
"""
72+
Each test should be invoked, resetting mocks in between each test. E.g.:
73+
self.test_x()
74+
self.reset_mocks()
75+
self.test_y()
76+
self.reset_mocks()
77+
etc.
78+
"""
79+
self._test_botocore_installed_flag()
80+
self._reset_mocks()
81+
82+
def _test_unpatched_botocore_instrumentation(self):
6483
# Kinesis
6584
self.assertFalse("kinesis" in _KNOWN_EXTENSIONS, "Upstream has added a Kinesis extension")
6685

@@ -74,7 +93,7 @@ def _validate_unpatched_botocore_instrumentation(self):
7493
self.assertFalse("aws.sqs.queue_url" in attributes)
7594
self.assertFalse("aws.sqs.queue_name" in attributes)
7695

77-
def _validate_patched_botocore_instrumentation(self):
96+
def _test_patched_botocore_instrumentation(self):
7897
# Kinesis
7998
self.assertTrue("kinesis" in _KNOWN_EXTENSIONS)
8099
kinesis_attributes: Dict[str, str] = _do_extract_kinesis_attributes()
@@ -96,6 +115,28 @@ def _validate_patched_botocore_instrumentation(self):
96115
self.assertTrue("aws.sqs.queue_name" in sqs_attributes)
97116
self.assertEqual(sqs_attributes["aws.sqs.queue_name"], _QUEUE_NAME)
98117

118+
def _test_botocore_installed_flag(self):
119+
with patch(
120+
"amazon.opentelemetry.distro.patches._botocore_patches._apply_botocore_instrumentation_patches"
121+
) as mock_apply_patches:
122+
get_distribution_patch: patch = self.method_patches[GET_DISTRIBUTION_PATCH]
123+
get_distribution_patch.side_effect = pkg_resources.DistributionNotFound
124+
apply_instrumentation_patches()
125+
mock_apply_patches.assert_not_called()
126+
127+
get_distribution_patch.side_effect = pkg_resources.VersionConflict("botocore==1.0.0", "botocore==0.0.1")
128+
apply_instrumentation_patches()
129+
mock_apply_patches.assert_not_called()
130+
131+
get_distribution_patch.side_effect = None
132+
get_distribution_patch.return_value = "CorrectDistributionObject"
133+
apply_instrumentation_patches()
134+
mock_apply_patches.assert_called()
135+
136+
def _reset_mocks(self):
137+
for method_patch in self.method_patches.values():
138+
method_patch.reset_mock()
139+
99140

100141
def _do_extract_kinesis_attributes() -> Dict[str, str]:
101142
service_name: str = "kinesis"

0 commit comments

Comments
 (0)