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.