Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Tests with javalin-testtools

So far we have checked the API by hand with curl. An automated test does the same with a program: it starts the server, sends requests and compares the responses with the expected ones. Tests run with one command and immediately show what broke after a change. Javalin has the javalin-testtools module: it starts the application on a free port, gives you a ready HTTP client and stops the server after the test.

Dependencies

Add the module and JUnit to pom.xml with the test scope — only tests need them:

<!-- pom.xml -->
<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin-testtools</artifactId>
    <version>${javalin.version}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.3</version>
    <scope>test</scope>
</dependency>

And the plugin that runs tests, in the plugins section:

<!-- pom.xml -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.6</version>
</plugin>

An application without starting it

A test needs a Javalin object that has not been started yet and has its own database. Move app creation from main into a createApp method:

// src/main/java/com/example/blog/Main.java
public static void main(String[] args) {
    var repo = new PostRepository(Database.create("jdbc:h2:./blog"));
    createApp(repo).start(8080);
}
 
public static Javalin createApp(PostRepository repo) {
    var events = new PostEvents();
    var posts = new PostController(repo, events);
    var chat = new Chat();
 
    return Javalin.create(config -> {
        // all the settings and routes from earlier lessons, unchanged
    });
}

main still starts the server on port 8080 with the file database, and the test passes an in-memory database to createApp.

The test

Create a class in the src/test/java folder:

// src/test/java/com/example/blog/PostApiTest.java
package com.example.blog;
 
import io.javalin.Javalin;
import io.javalin.testtools.JavalinTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
 
import java.util.Base64;
import java.util.UUID;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
class PostApiTest {
    private Javalin app;
 
    @BeforeEach
    void setUp() {
        String url = "jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1";
        app = Main.createApp(new PostRepository(Database.create(url)));
    }
 
    @Test
    void listIsEmptyAtStart() {
        JavalinTest.test(app, (server, client) -> {
            var response = client.get("/posts");
            assertEquals(200, response.code());
            assertEquals("[]", response.body().string());
        });
    }
 
    @Test
    void createNeedsLogin() {
        JavalinTest.test(app, (server, client) -> {
            var post = new NewPost("Test", "Some text", "Anna");
            assertEquals(401, client.post("/posts", post).code());
        });
    }
 
    @Test
    void annaCreatesPost() {
        JavalinTest.test(app, (server, client) -> {
            var post = new NewPost("Test", "Some text", "Anna");
            String auth = "Basic " + Base64.getEncoder().encodeToString("anna:changeme".getBytes());
            var response = client.post("/posts", post, req -> req.header("Authorization", auth));
            assertEquals(201, response.code());
            assertTrue(response.body().string().contains("\"title\":\"Test\""));
        });
    }
}

Before each test @BeforeEach creates the application with a new empty database, so tests do not affect each other. JavalinTest.test starts the server, passes a client to the lambda and stops the server at the end. client.post turns the object into JSON by itself. Paths are written without the server address: the client knows the chosen port.

Running

mvn test
[INFO] Running com.example.blog.PostApiTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.827 s -- in com.example.blog.PostApiTest
[INFO] BUILD SUCCESS

Common mistake

If the expectation does not match the response, JUnit shows both values. For example, the test expects 201, but the Authorization header was forgotten:

[ERROR] com.example.blog.PostApiTest.createNeedsLogin -- Time elapsed: 0.095 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <201> but was: <401>

The expected value goes first in assertEquals, the actual one second. If you swap them, the message becomes misleading.

Thanks for reading!

Donate to the site — we work hard for you.