diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml
index 29d0511358..f22e42a6c8 100644
--- a/sdk-tests/pom.xml
+++ b/sdk-tests/pom.xml
@@ -143,6 +143,11 @@
org.testcontainersjunit-jupiter
+
+ io.dapr
+ testcontainers-dapr
+ test
+ org.springframework.dataspring-data-keyvalue
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/actors/DaprActorsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/actors/DaprActorsIT.java
index ba8cec619c..fe90b452c1 100644
--- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/actors/DaprActorsIT.java
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/actors/DaprActorsIT.java
@@ -20,64 +20,32 @@
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
+import io.dapr.testcontainers.spring.DaprContainerFactory;
+import io.dapr.testcontainers.spring.DaprSidecarContainer;
+import io.dapr.testcontainers.spring.DaprSpringBootTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
-import org.springframework.test.context.DynamicPropertyRegistry;
-import org.springframework.test.context.DynamicPropertySource;
-import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
-import org.testcontainers.junit.jupiter.Container;
-import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Map;
-import java.util.Random;
import java.util.UUID;
-import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
import static org.junit.jupiter.api.Assertions.assertEquals;
-@SpringBootTest(
- webEnvironment = WebEnvironment.RANDOM_PORT,
- classes = {
- TestActorsApplication.class,
- TestDaprActorsConfiguration.class
- }
-)
-@Testcontainers
+@DaprSpringBootTest(classes = {TestActorsApplication.class, TestDaprActorsConfiguration.class})
@Tag("testcontainers")
public class DaprActorsIT {
- private static final Network DAPR_NETWORK = Network.newNetwork();
- private static final Random RANDOM = new Random();
- private static final int PORT = RANDOM.nextInt(1000) + 8000;
private static final String ACTORS_MESSAGE_PATTERN = ".*Actor runtime started.*";
- @Container
- private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
- .withAppName("actor-dapr-app")
- .withNetwork(DAPR_NETWORK)
- .withComponent(new Component("kvstore", "state.in-memory", "v1",
- Map.of("actorStateStore", "true")))
- .withDaprLogLevel(DaprLogLevel.DEBUG)
- .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
- .withAppChannelAddress("host.testcontainers.internal")
- .withAppPort(PORT);
-
- /**
- * Expose the Dapr ports to the host.
- *
- * @param registry the dynamic property registry
- */
- @DynamicPropertySource
- static void daprProperties(DynamicPropertyRegistry registry) {
- registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
- registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
- registry.add("server.port", () -> PORT);
- }
+ @DaprSidecarContainer
+ private static final DaprContainer DAPR_CONTAINER = DaprContainerFactory.createForSpringBootTest("actor-dapr-app")
+ .withComponent(new Component("kvstore", "state.in-memory", "v1",
+ Map.of("actorStateStore", "true")))
+ .withDaprLogLevel(DaprLogLevel.DEBUG)
+ .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()));
@Autowired
private ActorClient daprActorClient;
@@ -86,8 +54,8 @@ static void daprProperties(DynamicPropertyRegistry registry) {
private ActorRuntime daprActorRuntime;
@BeforeEach
- public void setUp(){
- org.testcontainers.Testcontainers.exposeHostPorts(PORT);
+ public void setUp() {
+ org.testcontainers.Testcontainers.exposeHostPorts(DAPR_CONTAINER.getAppPort());
daprActorRuntime.registerActor(TestActorImpl.class);
// Wait for actor runtime to start.
diff --git a/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprContainerFactory.java b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprContainerFactory.java
new file mode 100644
index 0000000000..fdf9815f46
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprContainerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 The Dapr 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
+ * http://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 io.dapr.testcontainers.spring;
+
+import io.dapr.testcontainers.DaprContainer;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+
+/**
+ * Factory for creating DaprContainer instances configured for Spring Boot integration tests.
+ *
+ *
This class handles the common setup required for bidirectional communication
+ * between Spring Boot applications and the Dapr sidecar in test scenarios.
+ */
+public final class DaprContainerFactory {
+
+ private DaprContainerFactory() {
+ // Utility class
+ }
+
+ /**
+ * Creates a DaprContainer pre-configured for Spring Boot integration tests.
+ * This factory method handles the common setup required for bidirectional
+ * communication between Spring Boot and the Dapr sidecar:
+ *
+ *
Allocates a free port for the Spring Boot application
+ *
Configures the app channel address for container-to-host communication
+ *
+ *
+ * @param appName the Dapr application name
+ * @return a pre-configured DaprContainer for Spring Boot tests
+ */
+ public static DaprContainer createForSpringBootTest(String appName) {
+ int port = allocateFreePort();
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName(appName)
+ .withAppPort(port)
+ .withAppChannelAddress("host.testcontainers.internal");
+ }
+
+ private static int allocateFreePort() {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ socket.setReuseAddress(true);
+ return socket.getLocalPort();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to allocate free port", e);
+ }
+ }
+}
diff --git a/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSidecarContainer.java b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSidecarContainer.java
new file mode 100644
index 0000000000..2fd2ee47f1
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSidecarContainer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2025 The Dapr 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
+ * http://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 io.dapr.testcontainers.spring;
+
+import org.testcontainers.junit.jupiter.Container;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a static field containing a {@link io.dapr.testcontainers.DaprContainer}
+ * for automatic integration with Spring Boot tests.
+ *
+ *
This annotation combines the Testcontainers {@link Container} annotation
+ * with Dapr-specific configuration. When used with {@link DaprSpringBootTest},
+ * it automatically:
+ *
+ *
Manages the container lifecycle via Testcontainers
+ *
Configures Spring properties (server.port, dapr.http.endpoint, dapr.grpc.endpoint)
+ *
+ *
+ *
Important: For tests that require Dapr-to-app communication (like actor tests),
+ * you must call {@code Testcontainers.exposeHostPorts(container.getAppPort())}
+ * in your {@code @BeforeEach} method before registering actors or making Dapr calls.
+ *
+ * @see DaprSpringBootTest
+ * @see io.dapr.testcontainers.DaprContainer#createForSpringBootTest(String)
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Container
+public @interface DaprSidecarContainer {
+}
diff --git a/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSpringBootContextInitializer.java b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSpringBootContextInitializer.java
new file mode 100644
index 0000000000..1fa0ffb453
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/testcontainers/spring/DaprSpringBootContextInitializer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2025 The Dapr 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
+ * http://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 io.dapr.testcontainers.spring;
+
+import io.dapr.testcontainers.DaprContainer;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.env.MapPropertySource;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Spring {@link ApplicationContextInitializer} that configures Dapr-related properties
+ * based on the {@link DaprContainer} registered by {@link DaprSpringBootExtension}.
+ *
+ *
This initializer sets the following properties:
+ *
+ *
{@code server.port} - The port allocated for the Spring Boot application
+ *
{@code dapr.http.endpoint} - The HTTP endpoint of the Dapr sidecar
+ *
{@code dapr.grpc.endpoint} - The gRPC endpoint of the Dapr sidecar
+ *
+ *
+ *
This initializer is automatically registered when using {@link DaprSpringBootTest}.
+ */
+public class DaprSpringBootContextInitializer
+ implements ApplicationContextInitializer {
+
+ private static final String PROPERTY_SOURCE_NAME = "daprTestcontainersProperties";
+
+ @Override
+ public void initialize(ConfigurableApplicationContext applicationContext) {
+ DaprContainer container = findContainer();
+
+ if (container == null) {
+ throw new IllegalStateException(
+ "No DaprContainer found in registry. Ensure you are using @DaprSpringBootTest "
+ + "with a @DaprSidecarContainer annotated field."
+ );
+ }
+
+ // Create a property source with lazy resolution for endpoints
+ // server.port can be resolved immediately since it's set at container creation time
+ // Dapr endpoints are resolved lazily since the container may not be started yet
+ applicationContext.getEnvironment().getPropertySources()
+ .addFirst(new DaprLazyPropertySource(PROPERTY_SOURCE_NAME, container));
+ }
+
+ private DaprContainer findContainer() {
+ // Return the first container in the registry
+ // In a test scenario, there should only be one test class running at a time
+ return DaprSpringBootExtension.CONTAINER_REGISTRY.values().stream()
+ .findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * Custom PropertySource that lazily resolves Dapr container endpoints.
+ * This allows the endpoints to be resolved after the container has started.
+ */
+ private static class DaprLazyPropertySource extends MapPropertySource {
+ private final Map> lazyProperties;
+
+ DaprLazyPropertySource(String name, DaprContainer container) {
+ super(name, new HashMap<>());
+
+ this.lazyProperties = new HashMap<>();
+ lazyProperties.put("server.port", container::getAppPort);
+ lazyProperties.put("dapr.http.endpoint", container::getHttpEndpoint);
+ lazyProperties.put("dapr.grpc.endpoint", container::getGrpcEndpoint);
+ }
+
+ @Override
+ public Object getProperty(String name) {
+ Supplier