Skip to content

Commit 53828cb

Browse files
committed
Merge branch '6.0.x'
2 parents e3c602c + aa20281 commit 53828cb

File tree

9 files changed

+278
-5
lines changed

9 files changed

+278
-5
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/spring-test.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dependencies {
5050
optional("io.projectreactor:reactor-test")
5151
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core")
5252
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
53+
optional('io.micrometer:context-propagation')
54+
optional('io.micrometer:micrometer-observation')
5355
testImplementation(project(":spring-core-test"))
5456
testImplementation(project(":spring-context-support"))
5557
testImplementation(project(":spring-oxm"))
@@ -58,7 +60,6 @@ dependencies {
5860
testImplementation(testFixtures(project(":spring-core")))
5961
testImplementation(testFixtures(project(":spring-tx")))
6062
testImplementation(testFixtures(project(":spring-web")))
61-
testImplementation('io.micrometer:context-propagation')
6263
testImplementation("jakarta.annotation:jakarta.annotation-api")
6364
testImplementation("javax.cache:cache-api")
6465
testImplementation("jakarta.ejb:jakarta.ejb-api")

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+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Observation support classes for the <em>Spring TestContext Framework</em>.
3+
*/
4+
@NonNullApi
5+
@NonNullFields
6+
package org.springframework.test.context.observation;
7+
8+
import org.springframework.lang.NonNullApi;
9+
import org.springframework.lang.NonNullFields;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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,\

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

Lines changed: 12 additions & 4 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.
@@ -34,6 +34,7 @@
3434
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
3535
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
3636
import org.springframework.test.context.web.ServletTestExecutionListener;
37+
import org.springframework.util.ClassUtils;
3738

3839
import static java.util.Arrays.asList;
3940
import static java.util.stream.Collectors.toList;
@@ -56,12 +57,16 @@
5657
*/
5758
class TestExecutionListenersTests {
5859

60+
private static final Class<?> micrometerListenerClass =
61+
ClassUtils.resolveClassName("org.springframework.test.context.observation.MicrometerObservationRegistryTestExecutionListener", null);
62+
5963
@Test
6064
void defaultListeners() {
6165
List<Class<?>> expected = asList(ServletTestExecutionListener.class,//
6266
DirtiesContextBeforeModesTestExecutionListener.class,//
6367
ApplicationEventsTestExecutionListener.class,//
6468
DependencyInjectionTestExecutionListener.class,//
69+
micrometerListenerClass,//
6570
DirtiesContextTestExecutionListener.class,//
6671
TransactionalTestExecutionListener.class,//
6772
SqlScriptsTestExecutionListener.class,//
@@ -80,6 +85,7 @@ void defaultListenersMergedWithCustomListenerPrepended() {
8085
DirtiesContextBeforeModesTestExecutionListener.class,//
8186
ApplicationEventsTestExecutionListener.class,//
8287
DependencyInjectionTestExecutionListener.class,//
88+
micrometerListenerClass,//
8389
DirtiesContextTestExecutionListener.class,//
8490
TransactionalTestExecutionListener.class,//
8591
SqlScriptsTestExecutionListener.class,//
@@ -97,6 +103,7 @@ void defaultListenersMergedWithCustomListenerAppended() {
97103
DirtiesContextBeforeModesTestExecutionListener.class,//
98104
ApplicationEventsTestExecutionListener.class,//
99105
DependencyInjectionTestExecutionListener.class,//
106+
micrometerListenerClass,//
100107
DirtiesContextTestExecutionListener.class,//
101108
TransactionalTestExecutionListener.class,
102109
SqlScriptsTestExecutionListener.class,//
@@ -116,6 +123,7 @@ void defaultListenersMergedWithCustomListenerInserted() {
116123
ApplicationEventsTestExecutionListener.class,//
117124
DependencyInjectionTestExecutionListener.class,//
118125
BarTestExecutionListener.class,//
126+
micrometerListenerClass,//
119127
DirtiesContextTestExecutionListener.class,//
120128
TransactionalTestExecutionListener.class,//
121129
SqlScriptsTestExecutionListener.class,//
@@ -361,9 +369,9 @@ static class BarTestExecutionListener extends AbstractTestExecutionListener {
361369

362370
@Override
363371
public int getOrder() {
364-
// 2500 is between DependencyInjectionTestExecutionListener (2000) and
365-
// DirtiesContextTestExecutionListener (3000)
366-
return 2500;
372+
// 2250 is between DependencyInjectionTestExecutionListener (2000) and
373+
// MicrometerObservationRegistryTestExecutionListener (2500)
374+
return 2250;
367375
}
368376
}
369377

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.util.HashMap;
20+
import java.util.Map;
21+
22+
import io.micrometer.observation.ObservationRegistry;
23+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
24+
import org.junit.jupiter.api.BeforeEach;
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.context.support.StaticApplicationContext;
28+
import org.springframework.test.context.TestContext;
29+
import org.springframework.test.context.TestExecutionListener;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.mockito.ArgumentMatchers.any;
33+
import static org.mockito.ArgumentMatchers.anyString;
34+
import static org.mockito.BDDMockito.given;
35+
import static org.mockito.BDDMockito.willAnswer;
36+
import static org.mockito.Mockito.mock;
37+
38+
/**
39+
* Unit tests for {@link MicrometerObservationRegistryTestExecutionListener}.
40+
*
41+
* @author Marcin Grzejszczak
42+
* @author Sam Brannen
43+
* @since 6.0.10
44+
*/
45+
class MicrometerObservationRegistryTestExecutionListenerTests {
46+
47+
private final ObservationRegistry originalObservationRegistry = globalObservationRegistry();
48+
49+
private final TestContext testContext = mock();
50+
51+
private final StaticApplicationContext applicationContext = new StaticApplicationContext();
52+
53+
private final Map<String, Object> attributes = new HashMap<>();
54+
55+
private final TestExecutionListener listener = new MicrometerObservationRegistryTestExecutionListener();
56+
57+
58+
@BeforeEach
59+
@SuppressWarnings({ "unchecked", "rawtypes" }) // for raw Class testClass
60+
void configureTestContextMock() {
61+
willAnswer(invocation -> attributes.put(invocation.getArgument(0), invocation.getArgument(1)))
62+
.given(testContext).setAttribute(anyString(), any());
63+
given(testContext.removeAttribute(anyString()))
64+
.willAnswer(invocation -> attributes.get(invocation.getArgument(0, String.class)));
65+
given(testContext.getApplicationContext()).willReturn(applicationContext);
66+
Class testClass = getClass();
67+
given(testContext.getTestClass()).willReturn(testClass);
68+
}
69+
70+
@Test
71+
void observationRegistryIsNotOverridden() throws Exception {
72+
assertGlobalObservationRegistryIsSameAsOriginal();
73+
74+
listener.beforeTestMethod(testContext);
75+
assertGlobalObservationRegistryIsSameAsOriginal();
76+
77+
listener.afterTestMethod(testContext);
78+
assertGlobalObservationRegistryIsSameAsOriginal();
79+
}
80+
81+
@Test
82+
void observationRegistryIsOverriddenByBeanFromApplicationContext() throws Exception {
83+
assertGlobalObservationRegistryIsSameAsOriginal();
84+
85+
ObservationRegistry testObservationRegistry = ObservationRegistry.create();
86+
applicationContext.getDefaultListableBeanFactory().registerSingleton("observationRegistry", testObservationRegistry);
87+
88+
listener.beforeTestMethod(testContext);
89+
ObservationRegistry globalObservationRegistry = globalObservationRegistry();
90+
assertThat(globalObservationRegistry)
91+
.as("The global ObservationRegistry should have been replaced with the one from the application context")
92+
.isNotSameAs(originalObservationRegistry)
93+
.isSameAs(testObservationRegistry);
94+
95+
listener.afterTestMethod(testContext);
96+
assertGlobalObservationRegistryIsSameAsOriginal();
97+
}
98+
99+
private void assertGlobalObservationRegistryIsSameAsOriginal() {
100+
assertThat(globalObservationRegistry()).isSameAs(originalObservationRegistry);
101+
}
102+
103+
private static ObservationRegistry globalObservationRegistry() {
104+
return ObservationThreadLocalAccessor.getInstance().getObservationRegistry();
105+
}
106+
107+
}

spring-test/src/test/resources/log4j2-test.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Logger name="org.springframework.test.context.cache" level="warn" />
1919
<Logger name="org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate" level="info" />
2020
<Logger name="org.springframework.test.context.junit4.rules" level="warn" />
21+
<Logger name="org.springframework.test.context.observation" level="warn" />
2122
<Logger name="org.springframework.test.context.transaction.TransactionalTestExecutionListener" level="warn" />
2223
<Logger name="org.springframework.test.context.web" level="warn" />
2324
<!-- The following must be kept at DEBUG in order to test SPR-14363. -->

0 commit comments

Comments
 (0)