Skip to content

Commit

Permalink
Merge pull request #13 from AxonIQ/docs
Browse files Browse the repository at this point in the history
Tutorial: Executing and Unit testing the RegisterBike functionality
  • Loading branch information
dgomezg authored Mar 15, 2024
2 parents 3590c01 + 9b23d57 commit 65ec1de
Show file tree
Hide file tree
Showing 13 changed files with 262 additions and 17 deletions.
2 changes: 1 addition & 1 deletion docs/_playbook/playbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ runtime:

ui:
bundle:
url: https://github.com/AxonIQ/axoniq-library-ui/releases/download/v.0.1.0/ui-bundle.zip
url: https://github.com/AxonIQ/axoniq-library-ui/releases/download/v.0.1.4/ui-bundle.zip
2 changes: 1 addition & 1 deletion docs/tutorial/antora.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: bikerental-demo
title: Building A Bike Rental Application
title: Building An Axon Framework Application From Scratch
version: true

asciidoc:
Expand Down
1 change: 1 addition & 0 deletions docs/tutorial/modules/ROOT/examples/root/requests.http
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion docs/tutorial/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
* xref:create-axon-framework-project.adoc[]
* xref:bootstraping-axonframework.adoc[]
* xref:implement-create-bike.adoc[]
* xref:run-app-with-docker-compose.adoc[]
* xref:run-app-with-docker-compose.adoc[]
* xref:invoking-create-bike-endpoint.adoc[]
* xref::unit-testing-commands.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ include::example$root/pom.xml[tags=dependencyManagement;deps-axon-bom;!*]

Now, we can declare the dependencies to use Axon Framework in any of the submodules of our project.


anchor:axon-dependencies[]

=== Declaring Axon dependencies in the rental module

Once the `axon-bom` is declared in the root project, we only need to specify the axon framework dependencies in the maven descriptor `pom.xml` file of the `rental` module.
Expand Down
113 changes: 113 additions & 0 deletions docs/tutorial/modules/ROOT/pages/invoking-create-bike-endpoint.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
:navtitle: Invoking the Register Bike Endpoint
:reftext: Invoking the Register Bike Endpoint

= Running the Register Bike Endpoint

Now that we have our xref::implement-create-bike.adoc[first feature implemented], and xref::run-app-with-docker-compose.adoc[our application up and running], we can try to invoke the endpoint to register a bike, and check the exchange of commands and events.

== Invoking the endpoint

To test our feature, we need to send the following HTTP request:


POST http://localhost:8080/bikes?bikeType={type}&location={city}

You can do this directly from the command line, any other HTTP REST client you are used to, or if you are using IntelliJ IDEA as your IDE, we will show you how to prepare a file to quickly execute the HTTP Endpoints we are going to use in this project.

=== From the command line

You can easily invoke the endpoint from the command line by typing the following `curl` command in your terminal.

[,console]
----
> curl -X POST "http://localhost:8080/bikes?bikeType=city&location=Utrecht"
----

The command will print out the response of the endpoint call, which, in our case, will be the UUID assigned to the new bike.

NOTE: Although we are sending a `POST HTTP` request, in this case we don't need to specify any content in the body request, as all the information is specified in the URL parameters. This way may not be the most recommended design for `POST` requests in `REST` services as the required information usually f from the request body and it is specified using either `-d`, `-F` or `--json` options in your `curl` command). Anyway, for simplicity's sake, we have designed our first `POST` endpoint to have all the required information in the URL parameters.

=== Using IntelliJ IDEA

If you are using IntelliJ IDEA as your IDE, you can benefit from its https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html[HTTP Client plugin,role=external,window=_blank] to define and invoke all the endpoints of your application.

You only need to create a file with `.http` or `.rest` extension. Then, you can add the URL of the different endpoints you want to invoke in your system. The various requests should be separated by a line with three hashtag marks (`\###`).

Start by creating a text file called `requests.http` in your root project, and add the following content to it:

:needs-improvement: change content block to include::example$root/requests.http[tag=registerBike] and substitute variables with double curly-braces such as {{rental}}
[source,httprequest]
./requests.http
----
### Register a new bike
POST http://localhost:8080/bikes?bikeType=city&location=Utrecht
###
----

Save the file, and you will see that IntelliJ IDEA now decorates the line containing the `POST` URL with a green "play" button. If you click on it while the 'rental' application is running, you will see how the request is being sent and executed.

[source]
----
POST http://localhost:8080/bikes?bikeType=city&location=Utrecht
HTTP/1.1 200 OK
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
9c27e890-8498-4329-afd6-58302b796fab
Response code: 200 (OK); Time: 107ms (107 ms); Content length: 36 bytes (36 B)
----

== Following the execution of the register bike request in Axon Server

After invoking the endpoint, we can check how our application processed the request and what messages were sent.

To do this, we will use the Axon Server dashboard. First, open your browser and go to the following URL

http://localhost:8024/

You will see the Axon Server dashboard, with the default instance and the `Rental Monolith` application connected.

image::image$AxonServer-Dashboard.png[Screenshot of the Axon Server dashboard, showing the Rental Application connected to the Axon Server instance]

=== Reviewing the commands

Click on the `Commands` button in the left panel. You will see all the command handlers registered by the application, which in this case is just one: our `RegisterBikeCommand`.

image::image$AxonServer-Commands.png[Screenshot of the Axon Server Commands panel, showing a table with all registered command handlers and the number of commands processed by the Rental Monolith application]

The table also shows that one `RegisterBikeCommand` was received and processed in the system by the `Rental Monolith` application, which corresponds to the command created by the `RentalController` and sent through the `CommandGateway`.

NOTE: If we had different applications connected to Axon Server, there would be a separate column for each one of them, and the column would show how many commands were received and processed by each application.

=== Reviewing the events

When we implemented the command handler, we validated the command and sent a `BikeRegisteredEvent` to notify any other component in the system of the change.

To see if this event was sent, click on the `Search` button on the left menu panel of the Axon Server Dashboard. This action will bring up a page allowing you to search for all events sent and stored in Axon Server.

Click on the Search button without specifying anything in the query field (leave it empty). You will see the only event that was sent as a result of running the command handler:

image::image$AxonServer-BikeRegisteredEvent.png[A screenshot of the Events Search page in Axon Server, showing the BikeRegisteredEvent]

Double-clicking on the event will bring up a popup panel showing all the event details.

image::image$AxonServer-BikeRegisteredEvent-details.png[A screenshot of the Events Search page in Axon Server, showing the details of the BikeRegisteredEvent]

This means that our system received the request to register a new bike and handled it correctly by creating and sending the appropriate command, and as a result of handling the command, created, sent and stored the `BikeRegisteredEvent`.

This last `BikeRegisteredEvent` will be reused for any component (such as any projection in the query model) that registers to receive this type of event. The command model will also use the' BikeRegisteredEvent' whenever the `Bike` entity needs to be rebuilt (or refreshed).

=== Conclusion

Thanks to the information provided by Axon Server, it's easy to check the commands, events (and queries) that have been sent and their contents. This is useful for checking the messages that our system exchanges to communicate the different modules.

However, we may also want to have some automated tests in the form of unit tests so that we don't have to do these manual checks every time.

In the next section, we will learn how to xref::unit-testing-commands.adoc[write a test case with Axon to ensure the command handler] processes the command correctly.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Even if we are going to run only one docker image (Axon Server), Docker Compose

We will start by creating the Docker Compose configuration file. Create a `compose.yaml` file in the `root` project with the following contents:

[sources,yml]
[sources,yaml]
.compose.yaml
----
include::example$root/compose.yaml[]
Expand Down
120 changes: 120 additions & 0 deletions docs/tutorial/modules/ROOT/pages/unit-testing-commands.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
:navtitle: Add Unit Tests For Command Handlers
:reftext: Create Unit Tests For Command Handling

= Testing Command Handlers

As our application grows, we won't be able to manually test that everything works correctly after a new feature. It is convenient to have a way to test automatically that our application works as expected.

In your applications, you probably already have some unit tests to check the business logic of your application.

:needs-improvement: Change ref to xref:axon_framework_ref::test:index.adoc[Test Module] when docs moved from the old docs to library
Axon Framework provides you with a https://go.axoniq.io/refguide/axon-framework/testing[Test Module,role=external,window=_blank] that will help you write unit tests designed to test the processing and handling of commands, events and queries in your application.


In this tutorial step, you will implement a unit test to check if our system processes the `RegisterBikeCommand` as expected.

== Configuring Axon Framework test support

Before writing our command handler test, we need to add the component Axon Framework provides to support unit testing.

We need to add the `org.axonframework:axon-test` dependency to the Maven file descriptor in our `rental` module to do this. Fortunately, we already added this dependency when xref:bootstraping-axonframework.adoc#axon-dependencies[we configured the Axon Framework dependencies].

Check that your `rental/pom.xml` file contains the reference to the `axon-test` module:

[,xml]
.rental/pom.xml
----
include::example$rental/pom.xml[tags=dependencies;deps-axonframework;!*,indent=0]
----

NOTE: The components provided by Axon Framework for unit testing are only needed during the `test` execution phase during the maven build process. Thus, we specify the `<scope>test</scope>` for the `axon-test` dependency.

With the `axon-test` component added to our project's dependency list we can create our command handler test.

== Creating the command `BikeTest`

To start writing our command handler test, create a new Java class with the name `BikeTest` in `/src/test/java/io/axoniq/demo/bikerental/rental/command` of your `rental` module.

TIP: You can also use your IDE to create the unit test class. Open the `Bike` class and ask your IDE to generate the corresponding unit test. Depending on your IDE, the shortcut or menu may vary, but it's a shortcut worth knowing for your IDE.

[,java]
./rental/src/test/java/io/axoniq/demo/bikerental/rental/command/BikeTest.java
----
include::example$rental/src/test/java/io/axoniq/demo/bikerental/rental/command/BikeTest.java[tags=BikeTestClass;!*]
----

== Defining the `AggregateTestFixture`

Axon Framework provides a component that allows you to create unit tests specifically focused on testing the behavior of handling a command by an aggregate. This component is called `AggregateTestFixture`.

NOTE: You can read more details about how an AggregateTestFixture works at the https://go.axoniq.io/refguide/axon-framework/testing/commands-events#command-model-testing["Command Model Testing" section of the Axon Framework reference guide,role=external,window=_blank]

:needs-improvement: update the url to xref section in the library once the old doc contents are moved to the library.

So, we need to add and initialize the `AggregateTestFixture` for the `Bike` class (the component that handles the command we want to test):

[,java]
./rental/src/test/java/io/axoniq/demo/bikerental/rental/command/BikeTest.java
----
include::example$rental/src/test/java/io/axoniq/demo/bikerental/rental/command/BikeTest.java[tags=BikeTestClass;BikeTestFixture;InitBikeTestFixture;!*]
----
<.> Define an `AggregateTestFixture` for the `Bike` aggregate. This is the "Subject Under Test" (or SUT) for our test.
<.> The `@BeforeEach` marks this method to be called before any test is executed in our test class. Adding the code to create the `AggregateTestFixture` here will ensure that we have a fresh fixture for each test case, and thus we make our different tests independent.
<.> This line creates a new `AggregateTestFixture` for our `Bike` class.

== Testing the command handler

Thanks to the help of the `AggregateTestFixture` we can now create a test with the following structure:

- *Given*: Set the initial state for our test. Since we are designing our system to follow Event-Sourcing patterns, we need to set the list of events that have already happened for the same aggregate (the bike) before receiving the command.
- *When*: Specify the command whose execution we want to test. In this case, we will test the processing of a `RegisterBikeCommand`.
- *Expect*: We can instruct the fixture on the expectations we have from our system after processing the command. In an Event-Sourcing system, we will specify these expectations in the form of what events should have been produced by the command handler as a result of processing the command.

So, let's define a method in our unit test to check that our system can successfully process the request to create a bike:

[,java]
----
include::example$rental/src/test/java/io/axoniq/demo/bikerental/rental/command/BikeTest.java[tags=BikeTestClass;RegisterBikeCommandHandlerTest;!*]
----
<.> In our case, when we receive the `RegisterBikeCommand`, we expect that no previous events were received in the system for the same `bikeId`.
<.> We provide the `RegisterBikeCommand` we want to send to the command handler.
<.> After successfully processing the `RegisterBikeCommand`, we expect the command handler to produce a new `BikeRegisteredEvent` with the details of the new bike.

The `AggregateTestFixture` will prepare the Bike aggregate to execute the command (in this case, this step is empty, as we specified that there had been no previous activity), execute the command handler for the `RegisterBikeCommand` and assert that the command handler has emitted the `BikeRegisteredEvent` with these specific values.

NOTE: You can learn more about the different things you can check from the fixture using `Matchers` in the section dedicated to https://go.axoniq.io/refguide/axon-framework/testing/commands-events#validation-phase[Validation-phase of the test fixture in the Axon Framework reference guide,role=external,window=_blank]

:needs-improvement: update the url to xref section in the library once the old doc contents are moved to the library.


== Executing the test

You can run the test manually from your IDE, and you should see that the test passes, meaning that the fixture has checked that the expectations are met after processing the `RegisterBikeCommand`.

In addition to running the test manually, we now have a test that is automatically run by Maven every time we build the application. If you run `mvn package` from your command line, you will see the execution of the test:

[,console]
----
% mvn package
[...]
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.axoniq.demo.bikerental.rental.command.BikeTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.120 s -- in io.axoniq.demo.bikerental.rental.command.BikeTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[...]
----

This automatic execution of the tests guarantees that if we break the expected behavior for this command handling while implementing a new feature in the future, we will know about it immediately.

With that certainty, we can move on and implement the next feature for our system.



Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,30 @@
import static org.axonframework.test.matchers.Matchers.matches;
import static org.axonframework.test.matchers.Matchers.messageWithPayload;

//tag::BikeTestClass[]
class BikeTest {

private AggregateTestFixture<Bike> fixture;
//tag::BikeTestFixture[]
private AggregateTestFixture<Bike> fixture; //<.>

@BeforeEach
//end::BikeTestFixture[]
//tag::InitBikeTestFixture[]
@BeforeEach //<.>
void setUp() {
fixture = new AggregateTestFixture<>(Bike.class);
fixture = new AggregateTestFixture<>(Bike.class); //<.>
}

//end::InitBikeTestFixture[]
//tag::RegisterBikeCommandHandlerTest[]
@Test
void canRegisterBike() {
fixture.givenNoPriorActivity()
.when(new RegisterBikeCommand("bikeId", "city", "Amsterdam"))
.expectEvents(new BikeRegisteredEvent("bikeId", "city", "Amsterdam"));
fixture.givenNoPriorActivity() //<.>
.when(new RegisterBikeCommand("bikeId-1234", "city-bike", "Amsterdam")) //<.>
.expectEvents(new BikeRegisteredEvent("bikeId-1234", "city-bike", "Amsterdam")); //<.>
}

//end::RegisterBikeCommandHandlerTest[]
//tag::otherTests[]
@Test
void canRequestAvailableBike() {
fixture.given(new BikeRegisteredEvent("bikeId", "city", "Amsterdam"))
Expand Down Expand Up @@ -137,7 +145,8 @@ void canRequestRejectedBike() {
e.bikeId().equals("bikeId")
&& e.renter().equals("newRider"))),
andNoMore()));


}
}
//end::otherTests[]

}
//end::BikeTestClass[]
5 changes: 1 addition & 4 deletions requests.http
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
### Register a new bike
# tag::registerBike[]
POST {{rental}}/bikes?bikeType=city&location=Madrid

### end::registerBike[]
POST {{rental}}/bikes?bikeType=city&location=Utrecht

### Generate bikes
# First, generate some bikes
Expand Down

0 comments on commit 65ec1de

Please sign in to comment.