Search code examples
javaakkaintegration-testingakka-http

Integration tests for akka http endpoints


I have an akka based application, and it has Application.java file which has structure like what we have defined here.

Now I want to write integration tests for akka http endpoints in this application, as discussed here.

It says that I need to launch application in @BeforeClass method.

So how do I start application. I guess merely calling main function wont work as it would block the program and tests would never hit.


Solution

  • I understand that you want to start a real http server for your integration test. You could extract the logic inside main method to another one and return the ActorSystem created.

        public static ActorSystem<NotUsed> startActorSystem() {
            //#server-bootstrapping
            Behavior<NotUsed> rootBehavior = Behaviors.setup(context -> {
                ActorRef<UserRegistry.Command> userRegistryActor =
                        context.spawn(UserRegistry.create(), "UserRegistry");
    
                UserRoutes userRoutes = new UserRoutes(context.getSystem(), userRegistryActor);
                startHttpServer(userRoutes.userRoutes(), context.getSystem());
    
                return Behaviors.empty();
            });
    
            // boot up server using the route as defined below
            return ActorSystem.create(rootBehavior, "HelloAkkaHttpServer");
            //#server-bootstrapping
        }
    

    Then, you can start the real server invoking that method

    var actorSystem = actorSystem = QuickstartApp.startActorSystem();
    

    Now you can send real http requests to the server instead of testing the routes isolated.

    Your new test should look like the following

    import akka.NotUsed;
    import akka.actor.typed.ActorSystem;
    import akka.http.javadsl.Http;
    import akka.http.javadsl.marshallers.jackson.Jackson;
    import akka.http.javadsl.model.HttpRequest;
    import akka.http.javadsl.model.MediaTypes;
    import org.junit.*;
    import org.junit.runners.MethodSorters;
    
    import java.util.concurrent.ExecutionException;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertTrue;
    
    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public class UserRoutesIntegrationTest {
    
        private static ActorSystem<NotUsed> actorSystem;
    
        @BeforeClass
        public static void beforeClass() {
            // ---------------------------
            // START THE REAL SERVER HERE
            // ---------------------------
            actorSystem = QuickstartApp.startActorSystem();
        }
    
        @AfterClass
        public static void afterClass() {
            // ------------------------------
            // SHUT DOWN THE REAL SERVER HERE
            // ------------------------------
            actorSystem.terminate();
        }
    
        @Test
        public void test1NoUsers() throws ExecutionException, InterruptedException {
            // ------------------------------------------
            // SEND REAL HTTP REQUEST TO REAL HTTP SERVER
            // ------------------------------------------
            var responseCompletionStage = Http
                .get(actorSystem)
                .singleRequest(HttpRequest.GET("http://localhost:8080/users"));
            // ---------------------------
            // UNMARSHAL THE HTTP RESPONSE
            // ---------------------------
            var usersCompletionStage = responseCompletionStage
                    .thenCompose(response -> Jackson.unmarshaller(UserRegistry.Users.class).unmarshal(response.entity(), actorSystem));
            var users = usersCompletionStage.toCompletableFuture().get();
            // ----------
            // ASSERTIONS
            // ----------
            assertTrue(users.users().isEmpty());
        }
    
        @Test
        public void test2HandlePOST() throws ExecutionException, InterruptedException {
            var responseCompletionStage = Http
                .get(actorSystem)
                .singleRequest(
                    HttpRequest
                        .POST("http://localhost:8080/users")
                        .withEntity(
                            MediaTypes.APPLICATION_JSON.toContentType(),
                            """
                                    {
                                        "name": "Kapi",
                                        "age": 42,
                                        "countryOfResidence": "jp"
                                    }
                                """
                        )
                    );
    
            var actionPerformedCompletionStage = responseCompletionStage
                    .thenCompose(response -> Jackson
                            .unmarshaller(UserRegistry.ActionPerformed.class)
                            .unmarshal(response.entity(), actorSystem));
            var actionPerformed = actionPerformedCompletionStage.toCompletableFuture().get();
            assertEquals("User Kapi created.", actionPerformed.description());
        }
    
        @Test
        public void test3Remove() throws ExecutionException, InterruptedException {
            var responseCompletionStage = Http
                    .get(actorSystem)
                    .singleRequest(HttpRequest.DELETE("http://localhost:8080/users/Kapi"));
            var actionPerformedCompletionStage = responseCompletionStage
                    .thenCompose(response -> Jackson
                            .unmarshaller(UserRegistry.ActionPerformed.class)
                            .unmarshal(response.entity(), actorSystem));
            var actionPerformed = actionPerformedCompletionStage.toCompletableFuture().get();
            assertEquals("User Kapi deleted.", actionPerformed.description());
        }
    }
    

    This is a dummy example of an integration test because you only need to start the http server. In case you need a database, a message queue or even another real server running in a docker container you could use something like Java Testcontainers.

    Have also in mind that you are hardcoding many things in your main class, such as the port or interface. It could be a good idea to do some refactor and receive some values or a Config object by param to make your tests more flexibles