mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Only update ObservationThreadLocalAccessor when test has an active ApplicationContext
Prior to this commit, MicrometerObservationRegistryTestExecutionListener always attempted to load the test's ApplicationContext in order to update the ObservationThreadLocalAccessor in its beforeTestMethod() callback, even if there was no active ApplicationContext. To avoid unnecessarily loading an ApplicationContext or attempting to load an ApplicationContext that cannot be loaded (for example, due to a context-load failure), this commit applies a hasApplicationContext() check in beforeTestMethod(). Since the MicrometerObservationRegistryTestExecutionListener is registered after the DependencyInjectionTestExecutionListener (at least by default), an active ApplicationContext should be present unless dependency injection from the context failed or the context failed to load. Closes gh-36817
This commit is contained in:
+9
-4
@@ -36,7 +36,8 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* <p>This listener updates the {@link ObservationThreadLocalAccessor} with the
|
||||
* {@code ObservationRegistry} obtained from the test's {@link ApplicationContext},
|
||||
* if present.
|
||||
* if the {@code ApplicationContext} is available and contains an
|
||||
* {@code ObservationRegistry} bean.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Sam Brannen
|
||||
@@ -125,9 +126,10 @@ class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExe
|
||||
}
|
||||
|
||||
/**
|
||||
* If the test's {@link ApplicationContext} contains an {@link ObservationRegistry}
|
||||
* bean, this method retrieves the {@code ObservationRegistry} currently stored
|
||||
* in {@link ObservationThreadLocalAccessor}, saves a reference to the original
|
||||
* If the test's {@link ApplicationContext} is available and contains an
|
||||
* {@link ObservationRegistry} bean, this method retrieves the
|
||||
* {@code ObservationRegistry} currently stored in
|
||||
* {@link ObservationThreadLocalAccessor}, saves a reference to the original
|
||||
* registry as a {@link TestContext} attribute (to be restored in
|
||||
* {@link #afterTestMethod(TestContext)}), and sets the registry from the test's
|
||||
* {@code ApplicationContext} in {@link ObservationThreadLocalAccessor}.
|
||||
@@ -136,6 +138,9 @@ class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExe
|
||||
*/
|
||||
@Override
|
||||
public void beforeTestMethod(TestContext testContext) {
|
||||
if (!testContext.hasApplicationContext()) {
|
||||
return;
|
||||
}
|
||||
testContext.getApplicationContext().getBeanProvider(ObservationRegistry.class)
|
||||
.ifAvailable(registry -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
+24
@@ -34,6 +34,8 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link MicrometerObservationRegistryTestExecutionListener}.
|
||||
@@ -41,6 +43,8 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Sam Brannen
|
||||
* @since 6.0.10
|
||||
* @see MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTests
|
||||
* @see MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTestNGTests
|
||||
*/
|
||||
class MicrometerObservationRegistryTestExecutionListenerTests {
|
||||
|
||||
@@ -62,11 +66,31 @@ class MicrometerObservationRegistryTestExecutionListenerTests {
|
||||
.given(testContext).setAttribute(anyString(), any());
|
||||
given(testContext.removeAttribute(anyString()))
|
||||
.willAnswer(invocation -> attributes.get(invocation.getArgument(0, String.class)));
|
||||
given(testContext.hasApplicationContext()).willReturn(true);
|
||||
given(testContext.getApplicationContext()).willReturn(applicationContext);
|
||||
Class testClass = getClass();
|
||||
given(testContext.getTestClass()).willReturn(testClass);
|
||||
}
|
||||
|
||||
@Test // gh-36817
|
||||
void beforeTestMethodIsNoOpWhenContextIsNotAvailable() throws Exception {
|
||||
given(testContext.hasApplicationContext()).willReturn(false);
|
||||
|
||||
listener.beforeTestMethod(testContext);
|
||||
|
||||
verify(testContext).hasApplicationContext();
|
||||
verify(testContext, never()).getApplicationContext();
|
||||
}
|
||||
|
||||
@Test // gh-36817
|
||||
void afterTestMethodIsNoOpWhenContextIsNotAvailable() throws Exception {
|
||||
given(testContext.hasApplicationContext()).willReturn(false);
|
||||
|
||||
listener.afterTestMethod(testContext);
|
||||
|
||||
verify(testContext, never()).getApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void observationRegistryIsNotOverridden() throws Exception {
|
||||
assertGlobalObservationRegistryIsSameAsOriginal();
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.observation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testng.TestNG;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
||||
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
|
||||
import org.springframework.test.context.testng.TrackingTestNGTestListener;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* JUnit based integration test which verifies that
|
||||
* {@link MicrometerObservationRegistryTestExecutionListener} — when used in
|
||||
* conjunction with Spring's TestNG support — does not attempt to load an
|
||||
* application context to update the
|
||||
* {@link io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor}
|
||||
* if the application context is not currently loaded or previously failed to load.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.1
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36817">gh-36817</a>
|
||||
* @see MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTests
|
||||
*/
|
||||
class MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTestNGTests {
|
||||
|
||||
/**
|
||||
* <p>NOTE: The {@code @BeforeMethod(alwaysRun = true)} and {@code @AfterMethod(alwaysRun = true)}
|
||||
* lifecycle methods in {@link AbstractTestNGSpringContextTests} are always invoked, even if a
|
||||
* previous lifecycle configuration method failed (for example, due to a context-load failure).
|
||||
*/
|
||||
@Test
|
||||
void contextLoadFailureCausesExpectedTestFailures() {
|
||||
TrackingTestNGTestListener listener = new TrackingTestNGTestListener();
|
||||
TestNG testNG = new TestNG();
|
||||
testNG.addListener(listener);
|
||||
testNG.setTestClasses(new Class<?>[] { ContextLoadFailureTestCase.class });
|
||||
testNG.setVerbose(0);
|
||||
testNG.run();
|
||||
|
||||
assertThat(listener.testStartCount).as("tests started").hasValue(2);
|
||||
assertThat(listener.testSuccessCount).as("tests succeeded").hasValue(0);
|
||||
assertThat(listener.testFailureCount).as("tests failed").hasValue(0);
|
||||
// Before the introduction of the hasApplicationContext() check in
|
||||
// MicrometerObservationRegistryTestExecutionListener, the @BeforeMethod lifecycle
|
||||
// method in AbstractTestNGSpringContextTests also attempted to load the faulty
|
||||
// ApplicationContext, resulting in 3 configuration failures:
|
||||
// 1 * @BeforeClass + 2 * @BeforeMethod = 3.
|
||||
// With the fix, only the @BeforeClass context-load failure is recorded.
|
||||
assertThat(listener.failedConfigurationsCount).as("failed configurations").hasValue(1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>The {@code @TestExecutionListeners} declaration replaces the default listeners with
|
||||
* only those needed to exercise {@link MicrometerObservationRegistryTestExecutionListener},
|
||||
* ensuring that no additional listeners call {@code testContext.getApplicationContext()}
|
||||
* without a conditional {@code hasApplicationContext()} check.
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@TestExecutionListeners({
|
||||
DependencyInjectionTestExecutionListener.class,
|
||||
MicrometerObservationRegistryTestExecutionListener.class
|
||||
})
|
||||
static class ContextLoadFailureTestCase extends AbstractTestNGSpringContextTests {
|
||||
|
||||
@org.testng.annotations.Test
|
||||
void test1() {
|
||||
}
|
||||
|
||||
@org.testng.annotations.Test
|
||||
void test2() {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
String alwaysFails() {
|
||||
throw new RuntimeException("Simulated context load failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.test.context.observation;
|
||||
|
||||
import org.junit.jupiter.api.MethodOrderer.MethodName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.junit.platform.testkit.engine.EngineTestKit;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
|
||||
|
||||
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||
import static org.junit.platform.testkit.engine.EventConditions.event;
|
||||
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
|
||||
import static org.junit.platform.testkit.engine.EventConditions.test;
|
||||
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
|
||||
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
|
||||
|
||||
/**
|
||||
* Tests which indirectly verify that {@link MicrometerObservationRegistryTestExecutionListener}
|
||||
* does not attempt to load an application context to update the
|
||||
* {@link io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor} if
|
||||
* the application context is not currently loaded or previously failed to load.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.1
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/issues/36817">gh-36817</a>
|
||||
* @see MicrometerObservationRegistryTestExecutionListenerTests
|
||||
* @see MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTestNGTests
|
||||
*/
|
||||
class MicrometerObservationRegistryTestExecutionListenerWithContextLoadFailureTests {
|
||||
|
||||
@Test
|
||||
void contextLoadFailureCausesExpectedTestFailures() {
|
||||
EngineTestKit.engine("junit-jupiter")
|
||||
.selectors(selectClass(ContextLoadFailureTestCase.class))
|
||||
.execute()
|
||||
.testEvents()
|
||||
.assertStatistics(stats -> stats.started(2).succeeded(0).failed(2))
|
||||
.assertThatEvents()
|
||||
.haveExactly(1, event(test("test1"),
|
||||
finishedWithFailure(
|
||||
instanceOf(IllegalStateException.class),
|
||||
message(msg -> msg.startsWith("Failed to load ApplicationContext")))))
|
||||
.haveExactly(1, event(test("test2"),
|
||||
finishedWithFailure(
|
||||
instanceOf(IllegalStateException.class),
|
||||
message(msg -> msg.contains("failure threshold")))));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>The {@code @TestExecutionListeners} declaration replaces the default listeners with
|
||||
* only those needed to exercise {@link MicrometerObservationRegistryTestExecutionListener},
|
||||
* ensuring that no additional listeners call {@code testContext.getApplicationContext()}
|
||||
* without a conditional {@code hasApplicationContext()} check.
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@TestExecutionListeners({
|
||||
DependencyInjectionTestExecutionListener.class,
|
||||
MicrometerObservationRegistryTestExecutionListener.class
|
||||
})
|
||||
@TestMethodOrder(MethodName.class)
|
||||
static class ContextLoadFailureTestCase {
|
||||
|
||||
@Test
|
||||
void test1() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void test2() {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
String alwaysFails() {
|
||||
throw new RuntimeException("Simulated context load failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user