Skip to content

Commit eef4c3c

Browse files
committed
Allow TestcontainersLifecycleBeanPostProcessor to detect scoped beans
Update `TestcontainersLifecycleBeanPostProcessor` so that scoped beans are included. Fixes gh-35786
1 parent 8bcdb4b commit eef4c3c

File tree

2 files changed

+206
-3
lines changed

2 files changed

+206
-3
lines changed

Diff for: spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ else if (this.startables.get() == Startables.STARTED) {
102102

103103
private void initializeStartables(Startable startableBean, String startableBeanName) {
104104
logger.trace(LogMessage.format("Initializing startables"));
105-
List<String> beanNames = new ArrayList<>(
106-
List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false)));
105+
List<String> beanNames = new ArrayList<>(getBeanNames(Startable.class));
107106
beanNames.remove(startableBeanName);
108107
List<Object> beans = getBeans(beanNames);
109108
if (beans == null) {
@@ -132,7 +131,7 @@ private void start(List<Object> beans) {
132131
private void initializeContainers() {
133132
if (this.containersInitialized.compareAndSet(false, true)) {
134133
logger.trace("Initializing containers");
135-
List<String> beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false));
134+
List<String> beanNames = getBeanNames(ContainerState.class);
136135
List<Object> beans = getBeans(beanNames);
137136
if (beans != null) {
138137
logger.trace(LogMessage.format("Initialized containers %s", beanNames));
@@ -144,6 +143,10 @@ private void initializeContainers() {
144143
}
145144
}
146145

146+
private List<String> getBeanNames(Class<?> type) {
147+
return List.of(this.beanFactory.getBeanNamesForType(type, true, false));
148+
}
149+
147150
private List<Object> getBeans(List<String> beanNames) {
148151
List<Object> beans = new ArrayList<>(beanNames.size());
149152
for (String beanName : beanNames) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright 2012-2024 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.boot.testcontainers.lifecycle;
18+
19+
import java.util.ArrayList;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.ExtendWith;
27+
import org.junit.jupiter.api.extension.ExtensionContext;
28+
import org.testcontainers.utility.DockerImageName;
29+
30+
import org.springframework.beans.factory.ObjectFactory;
31+
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderWithScopeIntegrationTests.AssertingSpringExtension;
32+
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderWithScopeIntegrationTests.ContainerConfig;
33+
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderWithScopeIntegrationTests.ScopedContextLoader;
34+
import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleOrderWithScopeIntegrationTests.TestConfig;
35+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
36+
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
37+
import org.springframework.boot.testsupport.container.RedisContainer;
38+
import org.springframework.boot.testsupport.container.TestImage;
39+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
40+
import org.springframework.context.annotation.Bean;
41+
import org.springframework.context.annotation.Configuration;
42+
import org.springframework.context.annotation.Scope;
43+
import org.springframework.context.support.GenericApplicationContext;
44+
import org.springframework.test.annotation.DirtiesContext;
45+
import org.springframework.test.context.ContextConfiguration;
46+
import org.springframework.test.context.junit.jupiter.SpringExtension;
47+
import org.springframework.test.context.support.AnnotationConfigContextLoader;
48+
import org.springframework.util.LinkedMultiValueMap;
49+
import org.springframework.util.MultiValueMap;
50+
51+
import static org.assertj.core.api.Assertions.assertThat;
52+
53+
/**
54+
* Integration tests for {@link TestcontainersLifecycleBeanPostProcessor} to ensure create
55+
* and destroy events happen in the correct order.
56+
*
57+
* @author Phillip Webb
58+
*/
59+
@ExtendWith(AssertingSpringExtension.class)
60+
@ContextConfiguration(loader = ScopedContextLoader.class, classes = { TestConfig.class, ContainerConfig.class })
61+
@DirtiesContext
62+
@DisabledIfDockerUnavailable
63+
class TestcontainersLifecycleOrderWithScopeIntegrationTests {
64+
65+
static List<String> events = Collections.synchronizedList(new ArrayList<>());
66+
67+
@Test
68+
void eventsAreOrderedCorrectlyAfterStartup() {
69+
assertThat(events).containsExactly("start-container", "create-bean");
70+
}
71+
72+
@Configuration(proxyBeanMethods = false)
73+
static class ContainerConfig {
74+
75+
@Bean
76+
@Scope("custom")
77+
@ServiceConnection("redis")
78+
RedisContainer redisContainer() {
79+
return TestImage.container(EventRecordingRedisContainer.class);
80+
}
81+
82+
}
83+
84+
@Configuration(proxyBeanMethods = false)
85+
static class TestConfig {
86+
87+
@Bean
88+
TestBean testBean() {
89+
events.add("create-bean");
90+
return new TestBean();
91+
}
92+
93+
}
94+
95+
static class TestBean implements AutoCloseable {
96+
97+
@Override
98+
public void close() throws Exception {
99+
events.add("destroy-bean");
100+
}
101+
102+
}
103+
104+
static class AssertingSpringExtension extends SpringExtension {
105+
106+
@Override
107+
public void afterAll(ExtensionContext context) throws Exception {
108+
super.afterAll(context);
109+
assertThat(events).containsExactly("start-container", "create-bean", "destroy-bean", "stop-container");
110+
}
111+
112+
}
113+
114+
static class EventRecordingRedisContainer extends RedisContainer {
115+
116+
EventRecordingRedisContainer(DockerImageName dockerImageName) {
117+
super(dockerImageName);
118+
}
119+
120+
@Override
121+
public void start() {
122+
events.add("start-container");
123+
super.start();
124+
}
125+
126+
@Override
127+
public void stop() {
128+
events.add("stop-container");
129+
super.stop();
130+
}
131+
132+
}
133+
134+
static class ScopedContextLoader extends AnnotationConfigContextLoader {
135+
136+
@Override
137+
protected GenericApplicationContext createContext() {
138+
CustomScope customScope = new CustomScope();
139+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext() {
140+
141+
@Override
142+
protected void onClose() {
143+
customScope.destroy();
144+
super.onClose();
145+
}
146+
147+
};
148+
context.getBeanFactory().registerScope("custom", customScope);
149+
return context;
150+
}
151+
152+
}
153+
154+
static class CustomScope implements org.springframework.beans.factory.config.Scope {
155+
156+
private Map<String, Object> instances = new HashMap<>();
157+
158+
private MultiValueMap<String, Runnable> destructors = new LinkedMultiValueMap<>();
159+
160+
@Override
161+
public Object get(String name, ObjectFactory<?> objectFactory) {
162+
return this.instances.computeIfAbsent(name, (key) -> objectFactory.getObject());
163+
}
164+
165+
@Override
166+
public Object remove(String name) {
167+
synchronized (this) {
168+
Object removed = this.instances.remove(name);
169+
this.destructors.get(name).forEach(Runnable::run);
170+
this.destructors.remove(name);
171+
return removed;
172+
}
173+
}
174+
175+
@Override
176+
public void registerDestructionCallback(String name, Runnable callback) {
177+
this.destructors.add(name, callback);
178+
}
179+
180+
@Override
181+
public Object resolveContextualObject(String key) {
182+
return null;
183+
}
184+
185+
@Override
186+
public String getConversationId() {
187+
return null;
188+
}
189+
190+
public void destroy() {
191+
synchronized (this) {
192+
this.destructors.forEach((name, actions) -> actions.forEach(Runnable::run));
193+
this.destructors.clear();
194+
this.instances.clear();
195+
}
196+
}
197+
198+
}
199+
200+
}

0 commit comments

Comments
 (0)