Skip to content

Commit 01f6b92

Browse files
artur-ciocanucicoyle
authored andcommitted
Adding DaprSpringBootTest and DaprSidecarContainer annotation for easier ITs authoring (dapr#1610)
* Adding DaprSpringBootTest and DaprSidecarContainer annotation for easier IT authoring. Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> # Conflicts: # testcontainers-dapr/pom.xml * Adding DaprSpringBootTest and DaprSidecarContainer annotation for easier IT authoring. Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> * Move all the helper Dapr SpringBoot annotations to tests, to avoid exposing it as public API Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> * Fix a few issues related to Dapr container usage in ITs. Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> * Addressing code review comments to ensure things are internal. Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> --------- Signed-off-by: Artur Ciocanu <artur.ciocanu@gmail.com> Co-authored-by: Cassie Coyle <cassie.i.coyle@gmail.com> Signed-off-by: salaboy <Salaboy@gmail.com>
1 parent a6f028e commit 01f6b92

File tree

8 files changed

+435
-54
lines changed

8 files changed

+435
-54
lines changed

sdk-tests/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@
143143
<groupId>org.testcontainers</groupId>
144144
<artifactId>junit-jupiter</artifactId>
145145
</dependency>
146+
<dependency>
147+
<groupId>io.dapr</groupId>
148+
<artifactId>testcontainers-dapr</artifactId>
149+
<scope>test</scope>
150+
</dependency>
146151
<dependency>
147152
<groupId>org.springframework.data</groupId>
148153
<artifactId>spring-data-keyvalue</artifactId>

sdk-tests/src/test/java/io/dapr/it/testcontainers/actors/DaprActorsIT.java

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,64 +20,32 @@
2020
import io.dapr.testcontainers.Component;
2121
import io.dapr.testcontainers.DaprContainer;
2222
import io.dapr.testcontainers.DaprLogLevel;
23+
import io.dapr.testcontainers.internal.DaprContainerFactory;
24+
import io.dapr.testcontainers.internal.DaprSidecarContainer;
25+
import io.dapr.testcontainers.internal.spring.DaprSpringBootTest;
2326
import org.junit.jupiter.api.BeforeEach;
2427
import org.junit.jupiter.api.Tag;
2528
import org.junit.jupiter.api.Test;
2629
import org.springframework.beans.factory.annotation.Autowired;
27-
import org.springframework.boot.test.context.SpringBootTest;
28-
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
29-
import org.springframework.test.context.DynamicPropertyRegistry;
30-
import org.springframework.test.context.DynamicPropertySource;
31-
import org.testcontainers.containers.Network;
3230
import org.testcontainers.containers.wait.strategy.Wait;
33-
import org.testcontainers.junit.jupiter.Container;
34-
import org.testcontainers.junit.jupiter.Testcontainers;
3531

3632
import java.util.Map;
37-
import java.util.Random;
3833
import java.util.UUID;
3934

40-
import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
4135
import static org.junit.jupiter.api.Assertions.assertEquals;
4236

43-
@SpringBootTest(
44-
webEnvironment = WebEnvironment.RANDOM_PORT,
45-
classes = {
46-
TestActorsApplication.class,
47-
TestDaprActorsConfiguration.class
48-
}
49-
)
50-
@Testcontainers
37+
@DaprSpringBootTest(classes = {TestActorsApplication.class, TestDaprActorsConfiguration.class})
5138
@Tag("testcontainers")
5239
public class DaprActorsIT {
53-
private static final Network DAPR_NETWORK = Network.newNetwork();
54-
private static final Random RANDOM = new Random();
55-
private static final int PORT = RANDOM.nextInt(1000) + 8000;
5640

5741
private static final String ACTORS_MESSAGE_PATTERN = ".*Actor runtime started.*";
5842

59-
@Container
60-
private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
61-
.withAppName("actor-dapr-app")
62-
.withNetwork(DAPR_NETWORK)
63-
.withComponent(new Component("kvstore", "state.in-memory", "v1",
64-
Map.of("actorStateStore", "true")))
65-
.withDaprLogLevel(DaprLogLevel.DEBUG)
66-
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
67-
.withAppChannelAddress("host.testcontainers.internal")
68-
.withAppPort(PORT);
69-
70-
/**
71-
* Expose the Dapr ports to the host.
72-
*
73-
* @param registry the dynamic property registry
74-
*/
75-
@DynamicPropertySource
76-
static void daprProperties(DynamicPropertyRegistry registry) {
77-
registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
78-
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
79-
registry.add("server.port", () -> PORT);
80-
}
43+
@DaprSidecarContainer
44+
private static final DaprContainer DAPR_CONTAINER = DaprContainerFactory.createForSpringBootTest("actor-dapr-app")
45+
.withComponent(new Component("kvstore", "state.in-memory", "v1",
46+
Map.of("actorStateStore", "true")))
47+
.withDaprLogLevel(DaprLogLevel.DEBUG)
48+
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()));
8149

8250
@Autowired
8351
private ActorClient daprActorClient;
@@ -86,8 +54,8 @@ static void daprProperties(DynamicPropertyRegistry registry) {
8654
private ActorRuntime daprActorRuntime;
8755

8856
@BeforeEach
89-
public void setUp(){
90-
org.testcontainers.Testcontainers.exposeHostPorts(PORT);
57+
public void setUp() {
58+
org.testcontainers.Testcontainers.exposeHostPorts(DAPR_CONTAINER.getAppPort());
9159
daprActorRuntime.registerActor(TestActorImpl.class);
9260

9361
// Wait for actor runtime to start.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.testcontainers.internal;
15+
16+
import io.dapr.testcontainers.DaprContainer;
17+
18+
import java.io.IOException;
19+
import java.net.ServerSocket;
20+
21+
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
22+
23+
/**
24+
* Factory for creating DaprContainer instances configured for Spring Boot integration tests.
25+
*
26+
* <p>This class handles the common setup required for bidirectional communication
27+
* between Spring Boot applications and the Dapr sidecar in test scenarios.</p>
28+
*/
29+
public final class DaprContainerFactory {
30+
31+
private DaprContainerFactory() {
32+
// Utility class
33+
}
34+
35+
/**
36+
* Creates a DaprContainer pre-configured for Spring Boot integration tests.
37+
* This factory method handles the common setup required for bidirectional
38+
* communication between Spring Boot and the Dapr sidecar:
39+
* <ul>
40+
* <li>Allocates a free port for the Spring Boot application</li>
41+
* <li>Configures the app channel address for container-to-host communication</li>
42+
* </ul>
43+
*
44+
* @param appName the Dapr application name
45+
* @return a pre-configured DaprContainer for Spring Boot tests
46+
*/
47+
public static DaprContainer createForSpringBootTest(String appName) {
48+
int port = allocateFreePort();
49+
50+
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
51+
.withAppName(appName)
52+
.withAppPort(port)
53+
.withAppChannelAddress("host.testcontainers.internal");
54+
}
55+
56+
private static int allocateFreePort() {
57+
try (ServerSocket socket = new ServerSocket(0)) {
58+
socket.setReuseAddress(true);
59+
return socket.getLocalPort();
60+
} catch (IOException e) {
61+
throw new IllegalStateException("Failed to allocate free port", e);
62+
}
63+
}
64+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.testcontainers.internal;
15+
16+
import io.dapr.testcontainers.internal.spring.DaprSpringBootTest;
17+
import org.testcontainers.junit.jupiter.Container;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Marks a static field containing a {@link io.dapr.testcontainers.DaprContainer}
26+
* for automatic integration with Spring Boot tests.
27+
*
28+
* <p>This annotation combines the Testcontainers {@link Container} annotation
29+
* with Dapr-specific configuration. When used with {@link DaprSpringBootTest},
30+
* it automatically:</p>
31+
* <ul>
32+
* <li>Manages the container lifecycle via Testcontainers</li>
33+
* <li>Configures Spring properties (server.port, dapr.http.endpoint, dapr.grpc.endpoint)</li>
34+
* </ul>
35+
*
36+
* <p><b>Important:</b> For tests that require Dapr-to-app communication (like actor tests),
37+
* you must call {@code Testcontainers.exposeHostPorts(container.getAppPort())}
38+
* in your {@code @BeforeEach} method before registering actors or making Dapr calls.</p>
39+
*
40+
* <p>Example usage:</p>
41+
* <pre>{@code
42+
* @DaprSpringBootTest(classes = MyApplication.class)
43+
* class MyDaprIT {
44+
*
45+
* @DaprSidecarContainer
46+
* private static final DaprContainer DAPR = DaprContainer.createForSpringBootTest("my-app")
47+
* .withComponent(new Component("statestore", "state.in-memory", "v1", Map.of()));
48+
*
49+
* @BeforeEach
50+
* void setUp() {
51+
* Testcontainers.exposeHostPorts(DAPR.getAppPort());
52+
* }
53+
*
54+
* @Test
55+
* void testSomething() {
56+
* // Your test code here
57+
* }
58+
* }
59+
* }</pre>
60+
*
61+
* @see DaprSpringBootTest
62+
* @see io.dapr.testcontainers.DaprContainer#createForSpringBootTest(String)
63+
*/
64+
@Target(ElementType.FIELD)
65+
@Retention(RetentionPolicy.RUNTIME)
66+
@Container
67+
public @interface DaprSidecarContainer {
68+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.testcontainers.internal.spring;
15+
16+
import io.dapr.testcontainers.DaprContainer;
17+
import org.springframework.context.ApplicationContextInitializer;
18+
import org.springframework.context.ConfigurableApplicationContext;
19+
import org.springframework.core.env.MapPropertySource;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
import java.util.function.Supplier;
24+
25+
/**
26+
* Spring {@link ApplicationContextInitializer} that configures Dapr-related properties
27+
* based on the {@link DaprContainer} registered by {@link DaprSpringBootExtension}.
28+
*
29+
* <p>This initializer sets the following properties:</p>
30+
* <ul>
31+
* <li>{@code server.port} - The port allocated for the Spring Boot application</li>
32+
* <li>{@code dapr.http.endpoint} - The HTTP endpoint of the Dapr sidecar</li>
33+
* <li>{@code dapr.grpc.endpoint} - The gRPC endpoint of the Dapr sidecar</li>
34+
* </ul>
35+
*
36+
* <p>This initializer is automatically registered when using {@link DaprSpringBootTest}.</p>
37+
*/
38+
public class DaprSpringBootContextInitializer
39+
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
40+
41+
private static final String PROPERTY_SOURCE_NAME = "daprTestcontainersProperties";
42+
43+
@Override
44+
public void initialize(ConfigurableApplicationContext applicationContext) {
45+
DaprContainer container = findContainer();
46+
47+
if (container == null) {
48+
throw new IllegalStateException(
49+
"No DaprContainer found in registry. Ensure you are using @DaprSpringBootTest "
50+
+ "with a @DaprSidecarContainer annotated field."
51+
);
52+
}
53+
54+
// Create a property source with lazy resolution for endpoints
55+
// server.port can be resolved immediately since it's set at container creation time
56+
// Dapr endpoints are resolved lazily since the container may not be started yet
57+
applicationContext.getEnvironment().getPropertySources()
58+
.addFirst(new DaprLazyPropertySource(PROPERTY_SOURCE_NAME, container));
59+
}
60+
61+
private DaprContainer findContainer() {
62+
// Return the first container in the registry
63+
// In a test scenario, there should only be one test class running at a time
64+
return DaprSpringBootExtension.CONTAINER_REGISTRY.values().stream()
65+
.findFirst()
66+
.orElse(null);
67+
}
68+
69+
/**
70+
* Custom PropertySource that lazily resolves Dapr container endpoints.
71+
* This allows the endpoints to be resolved after the container has started.
72+
*/
73+
private static class DaprLazyPropertySource extends MapPropertySource {
74+
private final Map<String, Supplier<Object>> lazyProperties;
75+
76+
DaprLazyPropertySource(String name, DaprContainer container) {
77+
super(name, new HashMap<>());
78+
79+
this.lazyProperties = new HashMap<>();
80+
lazyProperties.put("server.port", container::getAppPort);
81+
lazyProperties.put("dapr.http.endpoint", container::getHttpEndpoint);
82+
lazyProperties.put("dapr.grpc.endpoint", container::getGrpcEndpoint);
83+
}
84+
85+
@Override
86+
public Object getProperty(String name) {
87+
Supplier<Object> supplier = lazyProperties.get(name);
88+
if (supplier != null) {
89+
return supplier.get();
90+
}
91+
return null;
92+
}
93+
94+
@Override
95+
public boolean containsProperty(String name) {
96+
return lazyProperties.containsKey(name);
97+
}
98+
99+
@Override
100+
public String[] getPropertyNames() {
101+
return lazyProperties.keySet().toArray(new String[0]);
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)