Skip to content

Testing Your Handlers

This is where Luxis delivers on its promise. The same test code runs against an in-memory stub and a real Vert.x HTTP server. You write your tests once, and you choose the execution mode.

In-memory (stub)Real server (Vert.x)
FactoryLuxis.test(routes)Luxis.start(routes)
SpeedMillisecondsSeconds
NetworkNone — everything runs in-processReal HTTP over localhost
When to useDay-to-day development, CIFinal verification, debugging network-level issues

Both give you a TestClient with the same API. Your test code doesn’t know or care which mode it’s running in.

Define your routes in a method. This is the same function your production server calls:

public static MyState registerRoutes(RoutesRegister routesRegister) {
MyState state = new MyState();
routesRegister.jsonRoute("/echo", Method.POST, state, EchoRequest.class, new PostEchoHandler());
routesRegister.jsonRoute("/echo", Method.GET, state, Void.class, new GetEchoHandler());
return state;
}

In production:

Luxis.start(MyApp::registerRoutes);
Luxis<MyState> luxis = Luxis.test(MyApp::registerRoutes);
TestClient client = new StubTestClient("127.0.0.1", 8080, luxis);

No server starts. No ports are opened. Your handlers execute synchronously in-process.

Luxis<MyState> luxis = Luxis.start(MyApp::registerRoutes,
new WebServiceConfigBuilder().setPort(8080).build());
TestClient client = new VertxTestClient("127.0.0.1", 8080);

A real HTTP server starts on port 8080. Requests go over the network through the full Vert.x stack.

From this point on, the code is the same regardless of which mode you chose:

TestHttpResponse response = client.post(
StubRequest.request("/echo")
.body(json().put("name", "Alice").toString()));
Assert.assertEquals(
TestHttpResponse.response(expectedJson),
response);

Same request builder. Same response type. Same assertions. The only difference is how you created the Luxis instance — one line.

public class EchoTest {
private Luxis<MyState> luxis;
private TestClient client;
@Before
public void setUp() {
luxis = Luxis.test(MyApp::registerRoutes);
client = new StubTestClient("127.0.0.1", 8080, luxis);
}
@After
public void tearDown() throws Exception {
client.assertNoMoreExceptions();
client.close();
luxis.close();
}
@Test
public void shouldEchoBackJsonValues() {
String requestBody = json()
.put("intExample", 17)
.put("stringExample", "hiya")
.toString();
TestHttpResponse response = client.post(
StubRequest.request("/echo").body(requestBody));
String expectedResponse = json()
.put("intExample", 17)
.put("stringExample", "hiya")
.toString();
Assert.assertEquals(
TestHttpResponse.response(expectedResponse),
response);
}
}

To run this same test against the real Vert.x server, change two lines in setUp():

@Before
public void setUp() {
luxis = Luxis.start(MyApp::registerRoutes,
new WebServiceConfigBuilder().setPort(8080).build());
client = new VertxTestClient("127.0.0.1", 8080);
}

Everything else stays exactly the same.

Use StubRequest to build test requests fluently. This works in both modes:

StubRequest.request("/echo")
.body("{\"name\": \"test\"}")
.queryParam("search", "hello")
.headerParam("X-Request-Id", "abc")
.cookie("session", "xyz")
.fileUpload("document", "file contents")

TestHelper.json() returns a Jackson ObjectNode for fluent JSON construction:

import static io.kiw.luxis.web.test.TestHelper.json;
String body = json()
.put("intExample", 17)
.put("stringExample", "hiya")
.putNull("optionalField")
.toString();
// {"intExample":17,"stringExample":"hiya","optionalField":null}

TestHttpResponse supports equality checks on body, status code, headers, and cookies:

TestHttpResponse expected = TestHttpResponse.response(expectedJson)
.withStatusCode(400)
.withHeader("X-Custom", "value")
.withCookie(new HttpCookie("session", "abc"));
Assert.assertEquals(expected, actual);

The in-memory mode extends beyond a single service. You can wire multiple Luxis services together using LuxisHttpClient and test cross-service flows without any network:

// Create the downstream service in-memory
Luxis<DownstreamState> downstream = Luxis.test(DownstreamApp::registerRoutes);
// Create an HTTP client that talks to the downstream service in-memory
LuxisHttpClient httpClient = StubLuxisHttpClient.create(downstream);
// Create the upstream service, injecting the HTTP client
Luxis<UpstreamState> upstream = Luxis.test(r -> {
UpstreamState state = new UpstreamState();
r.jsonRoute("/call-next", Method.POST, state, HttpClientGetRequest.class,
new HttpClientCallHandler(httpClient, "http://127.0.0.1:8091"));
return state;
});
TestClient client = new StubTestClient("127.0.0.1", 8090, upstream);
// Test the full chain — no network, no Docker, milliseconds
TestHttpResponse response = client.post(
StubRequest.request("/call-next")
.body(json().put("targetPath", "/api/value").toString()));

In stub mode, all of this runs in-memory. To switch to real HTTP, replace Luxis.test with Luxis.start and StubLuxisHttpClient with VertxLuxisHttpClient. The test assertions don’t change.

In stub mode, exceptions thrown inside handlers are captured and can be asserted:

TestHttpResponse response = client.post(
StubRequest.request("/throw")
.body(json().put("where", "complete").toString()));
Assert.assertEquals(
TestHttpResponse.response(json().put("message", "Something went wrong").toString())
.withStatusCode(500),
response);
// Assert the exception was captured
client.assertException("app error in complete");
// At teardown, verify no unexpected exceptions occurred
client.assertNoMoreExceptions();