From 35d767f0d68ef5d4fb69fdde08f46d3a355ebba8 Mon Sep 17 00:00:00 2001 From: Andrzej Ludwikowski Date: Tue, 1 Aug 2023 13:02:48 +0200 Subject: [PATCH] docs: documenting ComponentClient --- docs/src/modules/java/nav.adoc | 2 +- docs/src/modules/java/pages/actions.adoc | 36 +++++++++- ....adoc => component-and-service-calls.adoc} | 28 ++++++-- .../java/pages/event-sourced-entities.adoc | 4 +- .../modules/java/pages/running-locally.adoc | 2 +- docs/src/modules/java/pages/value-entity.adoc | 2 +- ...iActionComponentClientIntegrationTest.java | 62 +++++++++++++++++ .../FibonacciActionIntegrationTest.java | 69 ++++++++++--------- .../example/fibonacci/FibonacciAction.java | 10 +-- .../fibonacci/LimitedFibonacciAction.java | 12 +++- 10 files changed, 177 insertions(+), 50 deletions(-) rename docs/src/modules/java/pages/{call-another-service.adoc => component-and-service-calls.adoc} (52%) create mode 100644 samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java diff --git a/docs/src/modules/java/nav.adoc b/docs/src/modules/java/nav.adoc index 230ae889b4..e0feced6a6 100644 --- a/docs/src/modules/java/nav.adoc +++ b/docs/src/modules/java/nav.adoc @@ -9,7 +9,7 @@ *** xref:java:actions.adoc[Actions] *** xref:java:actions-publishing-subscribing.adoc[Publishing and Subscribing] *** xref:java:timers.adoc[Timers] -*** xref:java:call-another-service.adoc[Calling other services] +*** xref:java:component-and-service-calls.adoc[Component and Service Calls] *** xref:java:failures-and-errors.adoc[Failures and Errors] *** xref:java:access-control.adoc[Using ACLs] *** xref:java:using-jwts.adoc[Using JWTs] \ No newline at end of file diff --git a/docs/src/modules/java/pages/actions.adoc b/docs/src/modules/java/pages/actions.adoc index 055f02c5a2..a74d02b679 100644 --- a/docs/src/modules/java/pages/actions.adoc +++ b/docs/src/modules/java/pages/actions.adoc @@ -82,7 +82,7 @@ include::example$java-spring-valueentity-shopping-cart/src/main/java/com/example <5> For allowed requests, use `componentClient` to get a deferred call to the entity. <6> The `deferredCall` is then used with `effects().forward()` to forward the request to the entity. -NOTE: NOTE: You might be wondering what the `componentClient` is about. For now, think of it as a lightweight, type safe, HTTP client allowing you to reach out to other Kalix services. All details can be found at xref:call-another-service.adoc[] chapter. +NOTE: NOTE: You might be wondering what the `componentClient` is about. For now, think of it as a lightweight, type safe, HTTP client allowing you to reach out to other Kalix services. All details can be found at xref:component-and-service-calls.adoc[] chapter. === Forwarding Headers @@ -247,9 +247,39 @@ Calling an action method through the test kit gives us back an link:{attachments * `getError()` returns the error description when `effects().error()` was returned to signal an error. * `getForward()` returns details about what message was forwarded and where the call was forwarded (since it is a unit test the forward is not actually executed). - - ifdef::todo[TODO: add links to before and after] NOTE: The side effects of an Action can NOT be tested in isolation at the moment. +=== Integration tests + +Actions (like any other Kalix component) can be verified with integration tests. The https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html[Spring WebClient] utility can be used to run any HTTP call to test Kalix components. + +[.tabset] +Java:: ++ +.src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java +[source,java] +---- +include::example$java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java[tag=testing-action] +---- +<1> Mark the test as a Spring integration tests. +<2> Set up the Kalix infrastructure by extending `KalixIntegrationTestKitSupport`. +<3> Use `WebClient` to call the Action component endpoint. + +In cases where detailed assertions on the HTTP response are not required, the Kalix xref:component-and-service-calls.adoc#_component_client[ComponentClient] can be used in integration tests. + + +[.tabset] +Java:: ++ +.src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java +[source,java] +---- +include::example$java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java[tag=testing-action] +---- +<1> Use the `ComponentClient` to call the Action component endpoint. +<2> Transform the `DeferredCall` to a `CompletionStage` and wait for the response. + + +NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`. diff --git a/docs/src/modules/java/pages/call-another-service.adoc b/docs/src/modules/java/pages/component-and-service-calls.adoc similarity index 52% rename from docs/src/modules/java/pages/call-another-service.adoc rename to docs/src/modules/java/pages/component-and-service-calls.adoc index 18f586d1cf..2252d001f1 100644 --- a/docs/src/modules/java/pages/call-another-service.adoc +++ b/docs/src/modules/java/pages/component-and-service-calls.adoc @@ -1,6 +1,28 @@ -= Calling other services -:page-aliases: spring:call-another-service.adoc += Component and Service Calls +:page-aliases: spring:component-and-service-calls.adoc +== Kalix components + +Since Kalix is an auto-scaling solution, components can be distributed across many nodes within the same service. Kalix doesn’t hide this fact and makes it explicit. That's why calling Kalix components is done via HTTP/gRPC `DeferredCall` calls. A `DeferredCall` is just an instruction on how to call a given component, which can be optimized by the Kalix engine (see xref:java:actions.adoc#_forwarding_commands[forwarding]). Sometimes it's necessary to transform the `DeferredCall` into the `CompletionStage` to combine many different component calls and build a single xref:java:actions.adoc#_composing_calls[asynchronous reply]. + +=== Component Client + +The Kalix `ComponentClient` is a utility for creating deferred calls in a type-safe way. You don't have to remember what the endpoint path is or which HTTP method should be selected for the call. Constructing the call is a matter of: + +* selecting the component type, +* choosing the endpoint, with a Java method reference, +* passing in parameters, if expected. + +[source,java,indent=0] +.src/main/java/com/example/fibonacci/LimitedFibonacciAction.java +---- +include::java:example$java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java[tag=component-client] +---- +<1> Select Action component. +<2> Select endpoint method reference. +<3> Pass in endpoint method arguments. + +NOTE: NOTE: Calling endpoints that return a stream response like `Flux` is not supported currently by the `ComponentClient`. == Kalix services @@ -17,8 +39,6 @@ In our delegating service implementation: ---- include::java:example$java-spring-doc-snippets/src/main/java/com/example/callanotherservice/DelegatingServiceAction.java[tag=delegating-action] ---- - - <1> Let the `WebClientProvider` be injected into the Action with constructor injection. <2> Use the `WebClientProvider` to build a `WebClient` for the *counter* service. <3> Use the `WebClient` to make a REST call to the *counter* service. diff --git a/docs/src/modules/java/pages/event-sourced-entities.adoc b/docs/src/modules/java/pages/event-sourced-entities.adoc index 8c69c0dc7d..8c45360902 100644 --- a/docs/src/modules/java/pages/event-sourced-entities.adoc +++ b/docs/src/modules/java/pages/event-sourced-entities.adoc @@ -192,7 +192,7 @@ For the above example, this class provides access to all the command handlers of === Integration tests -The skeleton of an Integration Test is generated for you if you use the archetype to start your Kalix app. Let's see what it could look like to test our Counter Entity: +The skeleton of an Integration Test is generated for you if you use the archetype to start your Kalix app. Let's see what it could look like to test our `ShoppingCartEntity`: ./src/it/java/com/example/IntegrationTest.java [source,java,indent=0] @@ -205,4 +205,4 @@ include::example$java-spring-eventsourced-shopping-cart/src/it/java/com/example/ <4> Request to add an item to the cart. <5> `GET` request to retrieve current status of the shopping cart and assert there should only be one item. -NOTE: The integration tests are under in a specific project profile `it` and can be run using `mvn verify -Pit`. +NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`. diff --git a/docs/src/modules/java/pages/running-locally.adoc b/docs/src/modules/java/pages/running-locally.adoc index 91fa060ab4..c131423d30 100644 --- a/docs/src/modules/java/pages/running-locally.adoc +++ b/docs/src/modules/java/pages/running-locally.adoc @@ -87,7 +87,7 @@ Use `Ctrl+C` to shut down the service. When stopping your service, it will also == Running multiple services locally -A typical Kalix application is composed of one or more services deployed to the same Kalix project. When deployed under the same Kalix project, two different services can make xref:call-another-service.adoc[calls to each other] or xref:service-to-service.adoc[subscribe to each other's event streams] by simply using their logical names. +A typical Kalix application is composed of one or more services deployed to the same Kalix project. When deployed under the same Kalix project, two different services can make xref:component-and-service-calls.adoc[calls to each other] or xref:service-to-service.adoc[subscribe to each other's event streams] by simply using their logical names. The same can be done on your local machine by configuring the services to run on different ports and by configuring them to "_discover_" each other using some extra configurations. diff --git a/docs/src/modules/java/pages/value-entity.adoc b/docs/src/modules/java/pages/value-entity.adoc index fedb8efffc..012424dca6 100644 --- a/docs/src/modules/java/pages/value-entity.adoc +++ b/docs/src/modules/java/pages/value-entity.adoc @@ -141,4 +141,4 @@ include::example$java-spring-valueentity-counter/src/it/java/com/example/Counter <4> Request to increase the value of counter `bar`. Response should have value `1`. <5> Explicit `GET` request to retrieve value of `bar` that should be `1`. -NOTE: The integration tests are under in a specific project profile `it` and can be run using `mvn verify -Pit`. +NOTE: The integration tests in samples are under in a specific project profile `it` and can be run using `mvn verify -Pit`. diff --git a/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java b/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java new file mode 100644 index 0000000000..28545bfce1 --- /dev/null +++ b/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionComponentClientIntegrationTest.java @@ -0,0 +1,62 @@ +package com.example.fibonacci; + +import com.example.Main; +import com.google.protobuf.any.Any; +import kalix.javasdk.DeferredCall; +import kalix.spring.testkit.KalixIntegrationTestKitSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +@DirtiesContext +// tag::testing-action[] +@SpringBootTest(classes = Main.class) +public class FibonacciActionComponentClientIntegrationTest extends KalixIntegrationTestKitSupport { + + private Duration timeout = Duration.of(5, SECONDS); + + @Test + public void calculateNextNumber() throws ExecutionException, InterruptedException, TimeoutException { + + Number response = componentClient.forAction() // <1> + .call(FibonacciAction::nextNumber) + .params(new Number(5)) + .execute() // <2> + .toCompletableFuture() + .get(timeout.toMillis(), MILLISECONDS); + + Assertions.assertEquals(8, response.value()); + } + // end::testing-action[] + + @Test + public void calculateNextNumberWithLimitedFibo() throws ExecutionException, InterruptedException, TimeoutException { + + Number response = componentClient.forAction() + .call(LimitedFibonacciAction::nextNumber) + .params(new Number(5)) + .execute() + .toCompletableFuture() + .get(timeout.toMillis(), MILLISECONDS); + + Assertions.assertEquals(8, response.value()); + } + + // tag::testing-action[] +} +// end::testing-action[] diff --git a/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java b/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java index e158c18168..2db296799d 100644 --- a/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java +++ b/samples/java-spring-fibonacci-action/src/it/java/com/example/fibonacci/FibonacciActionIntegrationTest.java @@ -6,7 +6,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.ErrorResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; @@ -15,55 +19,58 @@ import static java.time.temporal.ChronoUnit.SECONDS; -@SpringBootTest(classes = Main.class) -public class FibonacciActionIntegrationTest extends KalixIntegrationTestKitSupport { - +@DirtiesContext +// tag::testing-action[] +@SpringBootTest(classes = Main.class) // <1> +public class FibonacciActionIntegrationTest extends KalixIntegrationTestKitSupport { // <2> @Autowired private WebClient webClient; + private Duration timeout = Duration.of(5, SECONDS); + @Test public void calculateNextNumber() { - Mono response = - webClient.get() - .uri("/fibonacci/5/next") - .retrieve().bodyToMono(Number.class); - - long next = response.block(Duration.of(5, SECONDS)).value(); - Assertions.assertEquals(8, next); + ResponseEntity response = webClient.get() + .uri("/fibonacci/5/next") + .retrieve() + .toEntity(Number.class) + .block(timeout); // <3> + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); + Assertions.assertEquals(8, response.getBody().value()); } + // end::testing-action[] @Test public void calculateNextNumberWithLimitedFibo() { - Mono response = - webClient.get() - .uri("/limitedfibonacci/5/next") - .retrieve().bodyToMono(Number.class); - - long next = response.block(Duration.of(5, SECONDS)).value(); - Assertions.assertEquals(8, next); + ResponseEntity response = webClient.get() + .uri("/limitedfibonacci/5/next") + .retrieve() + .toEntity(Number.class) + .block(timeout); + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); + Assertions.assertEquals(8, response.getBody().value()); } @Test public void wrongNumberReturnsError() { - try { - - - ResponseEntity response = - webClient.get() - .uri("/fibonacci/7/next") - .retrieve().toEntity(Number.class) - .block(Duration.of(5, SECONDS)); - - Assertions.fail("Should have failed"); - } catch (WebClientResponseException.InternalServerError ex) { - String bodyErrorMessage = ex.getResponseBodyAsString(); - Assertions.assertTrue(bodyErrorMessage.contains("Input number is not a Fibonacci number")); - } + Mono> response = webClient.get() + .uri("/fibonacci/7/next") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> + clientResponse.bodyToMono(String.class) + .flatMap(error -> Mono.error(new RuntimeException(error))) + ) + .toBodilessEntity(); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> response.block(timeout)); + Assertions.assertEquals("Input number is not a Fibonacci number, received '7'", exception.getMessage()); } + // tag::testing-action[] } +// end::testing-action[] diff --git a/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/FibonacciAction.java b/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/FibonacciAction.java index 22aa60e8b8..5a7aacad3d 100644 --- a/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/FibonacciAction.java +++ b/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/FibonacciAction.java @@ -11,17 +11,19 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.PathVariable; - // end::implementing-action[] +import static io.grpc.Status.Code.INVALID_ARGUMENT; + + // tag::implementing-action[] @RequestMapping("/fibonacci") -public class FibonacciAction extends Action { +public class FibonacciAction extends Action { private boolean isFibonacci(long num) { // <1> Predicate isPerfectSquare = (n) -> { long square = (long) Math.sqrt(n); - return square*square == n; + return square * square == n; }; return isPerfectSquare.test(5*num*num + 4) || isPerfectSquare.test(5*num*num - 4); } @@ -42,7 +44,7 @@ public Effect nextNumber(@RequestBody Number number) { return effects().reply(new Number(nextFib(num))); } else { return effects() // <5> - .error("Input number is not a Fibonacci number, received '" + num + "'"); + .error("Input number is not a Fibonacci number, received '" + num + "'", INVALID_ARGUMENT); } } } diff --git a/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java b/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java index 61051f9709..cd14bc5d58 100644 --- a/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java +++ b/samples/java-spring-fibonacci-action/src/main/java/com/example/fibonacci/LimitedFibonacciAction.java @@ -1,6 +1,8 @@ package com.example.fibonacci; +import com.google.protobuf.any.Any; import io.grpc.Status; +import kalix.javasdk.DeferredCall; import kalix.javasdk.action.Action; import kalix.javasdk.action.ActionCreationContext; import kalix.javasdk.client.ComponentClient; @@ -24,14 +26,18 @@ public LimitedFibonacciAction(ActionCreationContext ctx, ComponentClient compone } @GetMapping("/{number}/next") - public Effect nextNumber(@PathVariable Long number) { + public Effect nextNumberPath(@PathVariable Long number) { if (number < 0 || number > 10000) { return effects().error("Only numbers between 0 and 10k are allowed", Status.Code.INVALID_ARGUMENT); } else { logger.info("Executing GET call to real /fibonacci = " + number); - var serviceCall = componentClient.forAction().call(FibonacciAction::getNumber).params(number); + // tag::component-client[] + DeferredCall deferredCall = componentClient.forAction() // <1> + .call(FibonacciAction::getNumber) // <2> + .params(number);// <3> - return effects().forward(serviceCall); + return effects().forward(deferredCall); + // end::component-client[] } }