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.

The test harness lives in a separate artifact, luxis-test-support. Add it to your pom.xml with <scope>test</scope> so it never reaches your production classpath:

<dependency>
<groupId>io.kiw.luxis</groupId>
<artifactId>luxis-test-support</artifactId>
<version>0.5.2</version>
<scope>test</scope>
</dependency>

luxis-test-support already depends on luxis, so you don’t need to declare it twice in your test classpath.

Everything below comes from the io.kiw.luxis.web.test package. Anything you find under io.kiw.luxis.web.test.internal is wiring used by the harness itself — it is not part of the supported API and may change between versions.

In-memory (stub)Real server (Vert.x)
FactoryTestLuxis.from(Luxis.app(routes))Luxis.app(routes).start()
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 that implements ApplicationRoutesRegister<APP>. 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.app(MyApp::registerRoutes).start();
import io.kiw.luxis.web.test.TestLuxis;
import io.kiw.luxis.web.test.StubTestClient;
Luxis<MyState> luxis = TestLuxis.from(Luxis.app(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. TestLuxis.from(...) accepts the same LuxisBuilder you’d configure for production — .withConfig(...), .withDatabase(...), .withEventPlatform(...) all work identically before you hand the builder over.

Luxis<MyState> luxis = Luxis.app(MyApp::registerRoutes)
.withConfig(new WebServiceConfigBuilder().setPort(8080).build())
.start();
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 = TestLuxis.from(Luxis.app(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.app(MyApp::registerRoutes)
.withConfig(new WebServiceConfigBuilder().setPort(8080).build())
.start();
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
TestLuxis<DownstreamState> downstream = TestLuxis.from(Luxis.app(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
TestLuxis<UpstreamState> upstream = TestLuxis.from(Luxis.<UpstreamState>app(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 TestLuxis.from(...) with .start() on the builder 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();