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!"; }}
@Testvoid greetsTheVisitor() { TestFixture.create(new HelloEndpoint()) .whenGet("/hello") .expectResult("Hello, world!");}class HelloEndpoint { @HandleGet("/hello") fun hello() = "Hello, world!"}
@Testfun greetsTheVisitor() { TestFixture.create(HelloEndpoint()) .whenGet("/hello") .expectResult("Hello, world!")}The fixture also supports request bodies, headers, user context and response assertions.
Example: creating with POST
Section titled âExample: creating with POSTâ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.
@Testvoid 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}@Testfun registerGame() { testFixture .whenPost("/games", "/game/game-details.json") .expectResult(GameId::class.java) .expectEvents(RegisterGame::class.java)}The corresponding handler is:
@HandlePost("/games")CompletableFuture<GameId> addGame(GameDetails details) { return Fluxzero.sendCommand(new RegisterGame(details));}@HandlePost("/games")fun addGame(details: GameDetails): CompletableFuture<GameId> { return Fluxzero.sendCommand(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" ]}Example: querying with GET
Section titled âExample: querying with GETâ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:
@Testvoid 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}@Testfun getGames() { testFixture .givenPost("/games", "/game/game-details.json") .whenGet("/games") .expectResult<List<Game>> { it.size == 1 } .expectWebResponse { it.status == 200 }}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));}@HandleGet@Path("/games")fun getGames(@QueryParam term: String): CompletableFuture<List<Game>> { return Fluxzero.query(FindGames(term))}Cookies between requests
Section titled âCookies between requestsâ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.
Testing error responses and exceptions
Section titled âTesting error responses and exceptionsâ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:
@Testvoid postReturnsError() { testFixture .whenPost("/error", "body") // Simulate POST request .expectExceptionalResult(IllegalCommandException.class) // Assert thrown exception .expectWebResponse(r -> r.getStatus() == 403); // Assert HTTP 403 response}@Testfun postReturnsError() { testFixture .whenPost("/error", "body") .expectExceptionalResult(IllegalCommandException::class.java) .expectWebResponse { it.status == 403 }}The corresponding handler might look like:
@HandlePost("/error")void postForError(String body) { throw new IllegalCommandException("error: " + body);}@HandlePost("/error")fun postForError(body: String) { throw IllegalCommandException("error: $body")}Path parameter substitution in tests
Section titled âPath parameter substitution in testsâ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}/buywill 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 bothtestFixture.whenPost("/games", "/game/game-details.json") // returns gameId .andThen() .whenPost("/games/{gameId}/buy") // uses gameId, returns orderId .andThen() .whenPost("/games/{gameId}/refund/{orderId}") // uses bothExternal HTTP stubs and consumers
Section titled âExternal HTTP stubs and consumersâ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(); }}
@Testvoid callsExternalApi() { TestFixture.create(new AppEndpoint(), new ExternalApiStub()) .whenGet("/app") .expectWebResult(response -> response.getStatus() == 204) .expectNoErrors();}class AppEndpoint { @HandleGet("/app") fun handle(): WebResponse = 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") fun handle(): WebResponse = WebResponse.builder().status(204).build()}
@Testfun callsExternalApi() { TestFixture.create(AppEndpoint(), ExternalApiStub()) .whenGet("/app") .expectWebResult { it.status == 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.
Defaults and registration
Section titled âDefaults and registrationâ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.
What this test does not simulate
Section titled âWhat this test does not simulateâ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.
Tests with a real network server
Section titled âTests with a real network serverâ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