Skip to content

Testing web endpoints

Fluxzero allows you to simulate and verify HTTP interactions as part of your test flows. Web requests can be tested like any other command, query, or event.

Start with one endpoint and one assertion. No HTTP server or port is needed:

class HelloEndpoint {
@HandleGet("/hello")
String hello() { return "Hello, world!"; }
}
@Test
void greetsTheVisitor() {
TestFixture.create(new HelloEndpoint())
.whenGet("/hello")
.expectResult("Hello, world!");
}

The fixture also supports request bodies, headers, user context and response assertions.

In a game application, a POST handler can dispatch a command to register a game. Register the endpoint and command behavior with testFixture; the example below checks the response and resulting event.

@Test
void registerGame() {
testFixture
.whenPost("/games", "/game/game-details.json") // Simulates POST with payload
.expectResult(GameId.class) // Asserts a result is returned
.expectEvents(RegisterGame.class); // Asserts the applied update was published as an event
}

The corresponding handler is:

@HandlePost("/games")
CompletableFuture<GameId> addGame(GameDetails details) {
return Fluxzero.sendCommand(new RegisterGame(details));
}

As always, the .json file is automatically loaded from the classpath, allowing you to cleanly separate test data:

📄 /game/game-details.json

{
"title": "Legend of the Skylands",
"description": "An epic singleplayer adventure with puzzles and secrets.",
"releaseDate": "2025-11-12T00:00:00Z",
"tags": [
"adventure",
"puzzle",
"singleplayer"
]
}

You can test GET endpoints just as easily. This example first registers a game via POST /games, then fetches the list of all games via GET /games and checks the result:

@Test
void getGames() {
testFixture
.givenPost("/games", "/game/game-details.json") // Precondition: register a game
.whenGet("/games") // Perform GET request
.<List<Game>>expectResult(r -> r.size() == 1) // Assert one game is returned
.expectWebResponse(r -> r.getStatus() == 200); // Assert the status of the response
}

In Java, the explicit .<List<Game>> cast is needed because whenGet(...) alone does not expose a concrete generic result type to the fixture.

This corresponds to the following handler method:

@HandleGet
@Path("/games")
CompletableFuture<List<Game>> getGames(@QueryParam String term) {
return Fluxzero.query(new FindGames(term));
}

The fixture automatically stores response cookies and sends them on subsequent requests. To test a request without a stored session cookie, call withoutCookies("session"); pass several names to remove several cookies, or call withoutCookies() to remove them all. Like withCookie(name, value), it returns the same fixture for chaining. Later responses can still store cookies. Explicit Cookie headers are unchanged; use withoutHeader("Cookie") to remove a header previously set with withHeader.

Fluxzero allows you to verify how your web endpoints handle exceptional scenarios. This includes asserting the exception type as well as inspecting the resulting HTTP status or body.

Here’s a test that triggers a 403 Forbidden error via IllegalCommandException:

@Test
void postReturnsError() {
testFixture
.whenPost("/error", "body") // Simulate POST request
.expectExceptionalResult(IllegalCommandException.class) // Assert thrown exception
.expectWebResponse(r -> r.getStatus() == 403); // Assert HTTP 403 response
}

The corresponding handler might look like:

@HandlePost("/error")
void postForError(String body) {
throw new IllegalCommandException("error: " + body);
}

Fluxzero supports placeholder substitution in endpoint paths based on results from earlier steps.

  • The result of the first when...() step is saved after .andThen() is called.
  • Later paths like /games/{gameId}/buy will have {gameId} filled using the .toString() value of the previous result.
  • All results are tracked in order and substituted by position, not type.
testFixture.whenPost("/games", "/game/game-details.json") // returns gameId
.andThen()
.whenPost("/games/{gameId}/buy") // uses gameId, returns orderId
.andThen()
.whenPost("/games/{gameId}/refund/{orderId}"); // uses both

Test calls to an external service with an absolute-URL handler. Give the stub a separate consumer when the application endpoint waits for its response, so they cannot block each other on a shared consumer:

class AppEndpoint {
@HandleGet("/app")
WebResponse handle() {
return Fluxzero.get().webRequestGateway()
.sendAndWait(WebRequest.get("https://api.example.com/check").build());
}
}
@Consumer(name = "external-api-stub")
class ExternalApiStub {
@HandleGet("https://api.example.com/check")
WebResponse handle() {
return WebResponse.builder().status(204).build();
}
}
@Test
void callsExternalApi() {
TestFixture.create(new AppEndpoint(), new ExternalApiStub())
.whenGet("/app")
.expectWebResult(response -> response.getStatus() == 204)
.expectNoErrors();
}

Repeat with createAsync when consumer/tracking behavior is relevant. If you cannot annotate the stub, select it through a builder-level ConsumerConfiguration instead.

Keep the application’s consumer configuration intact and register stubs before the first request. The fixture should exercise the same configuration as the application; see Message tracking for defaults and explicit consumers.

Stubs also use the normal namespace, authorization and interceptor rules. Test allowed and denied callers with a UserProvider that resolves their test identities. See Testing your handlers for fixture options.

The fixture routes native-HTTP settings to registered handlers too. It preserves the retry count/status policy without real retry delays; it does not test sockets, TLS, proxy behavior or remote transport envelopes. sendAndWait only changes how the caller waits. It does not make a real request local. Use transport tests for source/request-ID correlation and real HTTP tests for headers, body mapping and network failures.

When a test needs an embedded TestServer, bind it directly to loopback and port 0: TestServer.startServer(new InetSocketAddress("127.0.0.1", 0)) in Java, or TestServer.startServer(InetSocketAddress("127.0.0.1", 0)) in Kotlin. Read the selected port from the returned server’s ServerConnector.getLocalPort(), connect to 127.0.0.1, and stop the server in finally. This avoids fixed-port conflicts and the race introduced by reserving, closing, and later rebinding a free port.


© 2026 Fluxzero