Skip to content

Commit aa20281

Browse files
committed
Overhaul TestExecutionListener for Micrometer ObservationRegistry
This commit overhauls the TestExecutionListener for Micrometer's ObservationRegistry that was introduced in the previous commit. Specifically, this commit: - Renames the listener to MicrometerObservationRegistryTestExecutionListener since the use of a ThreadLocal is an implementation detail that may change over time. - Makes the listener package-private instead of public in order to allow the team greater flexibility in evolving this feature. - Eagerly loads the ObservationThreadLocalAccessor class and verifies that it has a getObservationRegistry() method to ensure that the listener is properly skipped when SpringFactoriesLoader attempts to load it, if Micrometer 1.10.8+ is not on the classpath. - Switches the listener's automatic registration order to 2500 in order to register it after the DependencyInjectionTestExecutionListener. - Only tracks the previous ObservationRegistry in beforeTestMethod() if the test's ApplicationContext contains an ObservationRegistry bean. - Properly removes the TestContext attribute for the previous ObservationRegistry in afterTestMethod(). - Introduces DEBUG logging for diagnostics. - Adds an entry in the Javadoc for TestExecutionListener as well as in the Testing chapter in the reference manual. Closes gh-30658
1 parent a82659c commit aa20281

File tree

9 files changed

+268
-187
lines changed

9 files changed

+268
-187
lines changed

framework-docs/modules/ROOT/pages/testing/testcontext-framework/tel-config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ by default, exactly in the following order:
1212
xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`].
1313
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
1414
instance.
15+
* `MicrometerObservationRegistryTestExecutionListener`: Provides support for
16+
Micrometer's `ObservationRegistry`.
1517
* `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for
1618
"`after`" modes.
1719
* `TransactionalTestExecutionListener`: Provides transactional test execution with

spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
* ApplicationEventsTestExecutionListener}</li>
6969
* <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener
7070
* DependencyInjectionTestExecutionListener}</li>
71+
* <li>{@link org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener
72+
* MicrometerObservationRegistryTestExecutionListener}</li>
7173
* <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener
7274
* DirtiesContextTestExecutionListener}</li>
7375
* <li>{@link org.springframework.test.context.transaction.TransactionalTestExecutionListener
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.observation;
18+
19+
import java.lang.reflect.Method;
20+
21+
import io.micrometer.observation.ObservationRegistry;
22+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
import org.junit.platform.launcher.TestExecutionListener;
26+
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.core.Conventions;
29+
import org.springframework.test.context.TestContext;
30+
import org.springframework.test.context.support.AbstractTestExecutionListener;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.ReflectionUtils;
33+
34+
/**
35+
* {@code TestExecutionListener} which provides support for Micrometer's
36+
* {@link ObservationRegistry}.
37+
*
38+
* <p>This listener updates the {@link ObservationThreadLocalAccessor} with the
39+
* {@code ObservationRegistry} obtained from the test's {@link ApplicationContext},
40+
* if present.
41+
*
42+
* @author Marcin Grzejszczak
43+
* @author Sam Brannen
44+
* @since 6.0.10
45+
*/
46+
class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExecutionListener {
47+
48+
private static final Log logger = LogFactory.getLog(MicrometerObservationRegistryTestExecutionListener.class);
49+
50+
private static final String OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME =
51+
"io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor";
52+
53+
/**
54+
* Attribute name for a {@link TestContext} attribute which contains the
55+
* {@link ObservationRegistry} that was previously stored in the
56+
* {@link ObservationThreadLocalAccessor}.
57+
* <p>After each test method, the previously stored {@code ObservationRegistry}
58+
* will be restored. If tests run concurrently this might cause issues unless
59+
* the {@code ObservationRegistry} is always the same (which should typically
60+
* be the case).
61+
*/
62+
private static final String PREVIOUS_OBSERVATION_REGISTRY = Conventions.getQualifiedAttributeName(
63+
MicrometerObservationRegistryTestExecutionListener.class, "previousObservationRegistry");
64+
65+
66+
static {
67+
// Trigger eager resolution of Micrometer Observation types during static
68+
// initialization of this class to ensure that this listener can be properly
69+
// skipped when SpringFactoriesLoader attempts to load it, if micrometer-observation
70+
// is not in the classpath or if the version of ObservationThreadLocalAccessor
71+
// present does not include the getObservationRegistry() method.
72+
String errorMessage =
73+
"MicrometerObservationRegistryTestExecutionListener requires micrometer-observation 1.10.8 or higher";
74+
Class<?> clazz;
75+
try {
76+
clazz = Class.forName(OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME, true,
77+
TestExecutionListener.class.getClassLoader());
78+
}
79+
catch (Throwable ex) {
80+
throw new IllegalStateException(errorMessage, ex);
81+
}
82+
83+
Method method = ReflectionUtils.findMethod(clazz, "getObservationRegistry");
84+
Assert.state(method != null, errorMessage);
85+
}
86+
87+
88+
/**
89+
* Returns {@code 2500}.
90+
*/
91+
@Override
92+
public final int getOrder() {
93+
return 2500;
94+
}
95+
96+
/**
97+
* If the test's {@link ApplicationContext} contains an {@link ObservationRegistry}
98+
* bean, this method retrieves the {@code ObservationRegistry} currently stored
99+
* in {@link ObservationThreadLocalAccessor}, saves a reference to the original
100+
* registry as a {@link TestContext} attribute (to be restored in
101+
* {@link #afterTestMethod(TestContext)}), and sets the registry from the test's
102+
* {@code ApplicationContext} in {@link ObservationThreadLocalAccessor}.
103+
* @param testContext the test context for the test; never {@code null}
104+
* @see #afterTestMethod(TestContext)
105+
*/
106+
@Override
107+
public void beforeTestMethod(TestContext testContext) {
108+
testContext.getApplicationContext().getBeanProvider(ObservationRegistry.class)
109+
.ifAvailable(registry -> {
110+
if (logger.isDebugEnabled()) {
111+
logger.debug("""
112+
Registering ObservationRegistry from ApplicationContext in \
113+
ObservationThreadLocalAccessor for test class \
114+
""" + testContext.getTestClass().getName());
115+
}
116+
ObservationThreadLocalAccessor accessor = ObservationThreadLocalAccessor.getInstance();
117+
testContext.setAttribute(PREVIOUS_OBSERVATION_REGISTRY, accessor.getObservationRegistry());
118+
accessor.setObservationRegistry(registry);
119+
});
120+
}
121+
122+
/**
123+
* Retrieves the original {@link ObservationRegistry} that was saved in
124+
* {@link #beforeTestMethod(TestContext)} and sets it in
125+
* {@link ObservationThreadLocalAccessor}.
126+
* @param testContext the test context for the test; never {@code null}
127+
* @see #beforeTestMethod(TestContext)
128+
*/
129+
@Override
130+
public void afterTestMethod(TestContext testContext) {
131+
ObservationRegistry previousObservationRegistry =
132+
(ObservationRegistry) testContext.removeAttribute(PREVIOUS_OBSERVATION_REGISTRY);
133+
if (previousObservationRegistry != null) {
134+
if (logger.isDebugEnabled()) {
135+
logger.debug("Restoring ObservationRegistry in ObservationThreadLocalAccessor for test class " +
136+
testContext.getTestClass().getName());
137+
}
138+
ObservationThreadLocalAccessor.getInstance().setObservationRegistry(previousObservationRegistry);
139+
}
140+
}
141+
142+
}

spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationThreadLocalTestExecutionListener.java

Lines changed: 0 additions & 92 deletions
This file was deleted.

spring-test/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ org.springframework.test.context.TestExecutionListener = \
55
org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener,\
66
org.springframework.test.context.event.ApplicationEventsTestExecutionListener,\
77
org.springframework.test.context.support.DependencyInjectionTestExecutionListener,\
8+
org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener,\
89
org.springframework.test.context.support.DirtiesContextTestExecutionListener,\
910
org.springframework.test.context.transaction.TransactionalTestExecutionListener,\
1011
org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener,\
11-
org.springframework.test.context.event.EventPublishingTestExecutionListener,\
12-
org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener
12+
org.springframework.test.context.event.EventPublishingTestExecutionListener
1313

1414
# Default ContextCustomizerFactory implementations for the Spring TestContext Framework
1515
#

spring-test/src/test/java/org/springframework/test/context/TestExecutionListenersTests.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,13 +28,13 @@
2828
import org.springframework.test.context.event.ApplicationEventsTestExecutionListener;
2929
import org.springframework.test.context.event.EventPublishingTestExecutionListener;
3030
import org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener;
31-
import org.springframework.test.context.observation.MicrometerObservationThreadLocalTestExecutionListener;
3231
import org.springframework.test.context.support.AbstractTestExecutionListener;
3332
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
3433
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
3534
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
3635
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
3736
import org.springframework.test.context.web.ServletTestExecutionListener;
37+
import org.springframework.util.ClassUtils;
3838

3939
import static java.util.Arrays.asList;
4040
import static java.util.stream.Collectors.toList;
@@ -57,14 +57,17 @@
5757
*/
5858
class TestExecutionListenersTests {
5959

60+
private static final Class<?> micrometerListenerClass =
61+
ClassUtils.resolveClassName("org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener", null);
62+
6063
@Test
6164
void defaultListeners() {
6265
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
6366
DirtiesContextBeforeModesTestExecutionListener.class,//
6467
ApplicationEventsTestExecutionListener.class,//
6568
DependencyInjectionTestExecutionListener.class,//
69+
micrometerListenerClass,//
6670
DirtiesContextTestExecutionListener.class,//
67-
MicrometerObservationThreadLocalTestExecutionListener.class,//
6871
TransactionalTestExecutionListener.class,//
6972
SqlScriptsTestExecutionListener.class,//
7073
EventPublishingTestExecutionListener.class
@@ -82,8 +85,8 @@ void defaultListenersMergedWithCustomListenerPrepended() {
8285
DirtiesContextBeforeModesTestExecutionListener.class,//
8386
ApplicationEventsTestExecutionListener.class,//
8487
DependencyInjectionTestExecutionListener.class,//
88+
micrometerListenerClass,//
8589
DirtiesContextTestExecutionListener.class,//
86-
MicrometerObservationThreadLocalTestExecutionListener.class,//
8790
TransactionalTestExecutionListener.class,//
8891
SqlScriptsTestExecutionListener.class,//
8992
EventPublishingTestExecutionListener.class
@@ -100,8 +103,8 @@ void defaultListenersMergedWithCustomListenerAppended() {
100103
DirtiesContextBeforeModesTestExecutionListener.class,//
101104
ApplicationEventsTestExecutionListener.class,//
102105
DependencyInjectionTestExecutionListener.class,//
106+
micrometerListenerClass,//
103107
DirtiesContextTestExecutionListener.class,//
104-
MicrometerObservationThreadLocalTestExecutionListener.class,//
105108
TransactionalTestExecutionListener.class,
106109
SqlScriptsTestExecutionListener.class,//
107110
EventPublishingTestExecutionListener.class,//
@@ -120,8 +123,8 @@ void defaultListenersMergedWithCustomListenerInserted() {
120123
ApplicationEventsTestExecutionListener.class,//
121124
DependencyInjectionTestExecutionListener.class,//
122125
BarTestExecutionListener.class,//
126+
micrometerListenerClass,//
123127
DirtiesContextTestExecutionListener.class,//
124-
MicrometerObservationThreadLocalTestExecutionListener.class,//
125128
TransactionalTestExecutionListener.class,//
126129
SqlScriptsTestExecutionListener.class,//
127130
EventPublishingTestExecutionListener.class
@@ -366,9 +369,9 @@ static class BarTestExecutionListener extends AbstractTestExecutionListener {
366369

367370
@Override
368371
public int getOrder() {
369-
// 2500 is between DependencyInjectionTestExecutionListener (2000) and
370-
// DirtiesContextTestExecutionListener (3000)
371-
return 2500;
372+
// 2250 is between DependencyInjectionTestExecutionListener (2000) and
373+
// MicrometerObservationRegistryTestExecutionListener (2500)
374+
return 2250;
372375
}
373376
}
374377

0 commit comments

Comments
 (0)