Skip to content

Commit

Permalink
Sort out a solution for testing that works for Spring Boot too
Browse files Browse the repository at this point in the history
  • Loading branch information
slinkydeveloper committed Nov 14, 2024
1 parent c7bcbee commit 0576768
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 122 deletions.
1 change: 1 addition & 0 deletions sdk-spring-boot-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies {

testAnnotationProcessor(project(":sdk-api-gen"))
testImplementation(project(":sdk-serde-jackson"))
testImplementation(project(":sdk-testing"))

testImplementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.springboot;

import static org.assertj.core.api.Assertions.assertThat;

import dev.restate.sdk.client.Client;
import dev.restate.sdk.testing.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(
classes = Greeter.class,
properties = {"greetingPrefix=Something something "})
@RestateTest(restateContainerImage = "ghcr.io/restatedev/restate:main")
public class SdkTestingIntegrationTest {

@Autowired @BindService private Greeter greeter;

@Test
@Timeout(value = 10)
void greet(@RestateClient Client ingressClient) {
var client = greeterClient.fromClient(ingressClient);

assertThat(client.greet("Francesco")).isEqualTo("Something something Francesco");
}
}

This file was deleted.

21 changes: 21 additions & 0 deletions sdk-testing/src/main/java/dev/restate/sdk/testing/BindService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testing;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @see RestateTest
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindService {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testing;

import java.util.List;
import org.junit.jupiter.api.extension.*;
import org.junit.platform.commons.support.AnnotationSupport;

/**
* @see RestateTest
*/
public class RestateExtension implements BeforeAllCallback, ParameterResolver {

static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(RestateExtension.class);
static final String RUNNER = "Runner";

@Override
public void beforeAll(ExtensionContext extensionContext) {
extensionContext
.getStore(NAMESPACE)
.getOrComputeIfAbsent(
RUNNER, ignored -> initializeRestateRunner(extensionContext), RestateRunner.class)
.beforeAll(extensionContext);
}

@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return extensionContext
.getStore(NAMESPACE)
.getOrComputeIfAbsent(
RUNNER, ignored -> initializeRestateRunner(extensionContext), RestateRunner.class)
.supportsParameter(parameterContext, extensionContext);
}

@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return extensionContext
.getStore(NAMESPACE)
.getOrComputeIfAbsent(
RUNNER, ignored -> initializeRestateRunner(extensionContext), RestateRunner.class)
.resolveParameter(parameterContext, extensionContext);
}

private RestateRunner initializeRestateRunner(ExtensionContext extensionContext) {
// Discover services
List<Object> servicesToBind =
AnnotationSupport.findAnnotatedFieldValues(
extensionContext.getRequiredTestInstance(), BindService.class);
if (servicesToBind.isEmpty()) {
throw new IllegalStateException(
"The class "
+ extensionContext.getRequiredTestClass().getName()
+ " is annotated with @RestateTest, but there are no fields annotated with @BindService");
}

RestateTest testAnnotation =
AnnotationSupport.findAnnotation(extensionContext.getRequiredTestClass(), RestateTest.class)
.orElseThrow(
() ->
new IllegalStateException(
"Expecting @RestateTest annotation on the test class"));

// Build runner discovering services to bind
var runnerBuilder = RestateRunnerBuilder.create();
servicesToBind.forEach(runnerBuilder::bind);
runnerBuilder.withRestateContainerImage(testAnnotation.restateContainerImage());
return runnerBuilder.buildRunner();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testing;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import dev.restate.sdk.client.Client;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.junit.jupiter.api.extension.*;

/**
* Restate runner for JUnit 5. Example:
Expand Down Expand Up @@ -39,8 +42,16 @@
* long response = client.get();
* assertThat(response).isEqualTo(0L);
* }</pre>
*
* @deprecated We now recommend using {@link RestateTest}.
*/
public class RestateRunner extends BaseRestateRunner implements BeforeAllCallback {
@Deprecated
public class RestateRunner implements BeforeAllCallback, ParameterResolver {

static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(RestateRunner.class);
static final String DEPLOYER_KEY = "Deployer";

private final ManualRestateRunner deployer;

RestateRunner(ManualRestateRunner deployer) {
Expand All @@ -52,4 +63,56 @@ public void beforeAll(ExtensionContext context) {
deployer.start();
context.getStore(NAMESPACE).put(DEPLOYER_KEY, deployer);
}

@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return supportsParameter(parameterContext);
}

static boolean supportsParameter(ParameterContext parameterContext) {
return (parameterContext.isAnnotated(RestateAdminClient.class)
&& dev.restate.admin.client.ApiClient.class.isAssignableFrom(
parameterContext.getParameter().getType()))
|| (parameterContext.isAnnotated(RestateClient.class)
&& Client.class.isAssignableFrom(parameterContext.getParameter().getType()))
|| (parameterContext.isAnnotated(RestateURL.class)
&& (String.class.isAssignableFrom(parameterContext.getParameter().getType())
|| URL.class.isAssignableFrom(parameterContext.getParameter().getType())));
}

@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
if (parameterContext.isAnnotated(RestateAdminClient.class)) {
return getDeployer(extensionContext).getAdminClient();
} else if (parameterContext.isAnnotated(RestateClient.class)) {
return resolveClient(extensionContext);
} else if (parameterContext.isAnnotated(RestateURL.class)) {
URL url = getDeployer(extensionContext).getIngressUrl();
if (parameterContext.getParameter().getType().equals(String.class)) {
return url.toString();
}
if (parameterContext.getParameter().getType().equals(URI.class)) {
try {
return url.toURI();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
return url;
}
throw new ParameterResolutionException("The parameter is not supported");
}

private Client resolveClient(ExtensionContext extensionContext) {
URL url = getDeployer(extensionContext).getIngressUrl();
return Client.connect(url.toString());
}

private ManualRestateRunner getDeployer(ExtensionContext extensionContext) {
return (ManualRestateRunner) extensionContext.getStore(NAMESPACE).get(DEPLOYER_KEY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
public class RestateRunnerBuilder {

private static final String DEFAULT_RESTATE_CONTAINER = "docker.io/restatedev/restate";
private static final String DEFAULT_RESTATE_CONTAINER = "docker.io/restatedev/restate:latest";
private final RestateHttpEndpointBuilder endpointBuilder;
private String restateContainerImage = DEFAULT_RESTATE_CONTAINER;
private final Map<String, String> additionalEnv = new HashMap<>();
Expand Down Expand Up @@ -90,7 +90,9 @@ public ManualRestateRunner buildManualRunner() {

/**
* @return a {@link RestateRunner} to be used as JUnit 5 Extension.
* @deprecated If you use JUnit 5, use {@link RestateTest}
*/
@Deprecated
public RestateRunner buildRunner() {
return new RestateRunner(this.buildManualRunner());
}
Expand Down
71 changes: 71 additions & 0 deletions sdk-testing/src/main/java/dev/restate/sdk/testing/RestateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
//
// This file is part of the Restate Java SDK,
// which is released under the MIT license.
//
// You can find a copy of the license in file LICENSE in the root
// directory of this repository or package, or at
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
package dev.restate.sdk.testing;

import java.lang.annotation.*;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;

/**
* Annotation to enable the Restate extension for JUnit 5. The annotation will bootstrap a Restate
* environment using TestContainers, and will automatically register all services field of the class
* annotated with {@link BindService}.
*
* <p>Example:
*
* <pre>
* // Annotate the class as RestateTest to start a Restate environment
* {@code @RestateTest}
* class CounterTest {
*
* // Annotate the service to bind
* {@code @BindService} private final Counter counter = new Counter();
*
* // Inject the client to send requests
* {@code @Test}
* void testGreet({@code @RestateClient} Client ingressClient) {
* var client = CounterClient.fromClient(ingressClient, "my-counter");
*
* long response = client.get();
* assertThat(response).isEqualTo(0L);
* }
* }
* </pre>
*
* <p>The runner will deploy the services locally, execute Restate as container using <a
* href="https://java.testcontainers.org/">Testcontainers</a>, and register the services.
*
* <p>This extension is scoped per test class, meaning that the restate runner will be shared among
* test methods. Because of the aforementioned issue, the extension sets the {@link TestInstance}
* {@link TestInstance.Lifecycle#PER_CLASS} automatically.
*
* <p>Use the annotations {@link RestateClient}, {@link RestateURL} and {@link RestateAdminClient}
* to interact with the deployed environment:
*
* <pre>
* {@code @Test}
* void initialCountIsZero({@code @RestateClient} Client client) {
* var client = CounterClient.fromClient(ingressClient, "my-counter");
*
* // Use client as usual
* long response = client.get();
* assertThat(response).isEqualTo(0L);
* }</pre>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ExtendWith(RestateExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public @interface RestateTest {

/** Restate container image to use */
String restateContainerImage() default "docker.io/restatedev/restate:latest";
}
Loading

0 comments on commit 0576768

Please sign in to comment.