Declare expected behavior as a state machine. Each step progresses when its condition is met — not when a timer fires. Xcepto handles retries, timeouts, and cleanup so your tests stay declarative and environment-independent.
var scenario = new ShipmentScenario();
await XceptoTest.Given(scenario, builder =>
{
var rest = builder.RestAdapterBuilder()
.WithBaseUrl(scenario.ApiAddress)
.WithSerializer(new NewtonsoftSerializer())
.Build();
rest.Post("/shipment/accept")
.WithRequestBody(() => new AcceptShipmentRequest(50))
.WithResponseType<AcceptShipmentResponse>()
.AssertThatResponse(r => r.Amount, Is.EqualTo(50));
// Retried until the assertion passes — no sleep needed
rest.Get("/inventory/stock")
.WithResponseType<StockResponse>()
.AssertThatResponse(r => r.Replenished, Is.True);
});
ShipmentScenario scenario = new ShipmentScenario();
Xcepto.given(scenario, builder -> {
var rest = new RestAdapterBuilder(builder)
.withBaseUrl(scenario.apiAddress)
.withSerializer(new GsonSerializer())
.build();
rest.post("/shipment/accept")
.withRequestBody(() -> new AcceptShipmentRequest(50))
.withResponseType(AcceptShipmentResponse.class)
.assertThatResponse(r -> r.amount == 50);
// Retried until the assertion passes — no sleep needed
rest.get("/inventory/stock")
.withResponseType(StockResponse.class)
.assertThatResponse(r -> r.replenished);
}, TIMEOUT, Duration.ofMillis(100));
Given-When-Then is a mindset, not just syntax. POST and PATCH execute exactly once — actions that change state. GET, PUT, and DELETE retry automatically until their assertions pass or the timeout expires. No Thread.Sleep, no polling loops — just causal transitions that match how distributed systems actually behave.
// POST executes once — assertion failure is immediate
rest.Post("/shipment/accept")
.WithRequestBody(() => new AcceptShipmentRequest(50))
.WithResponseType<AcceptShipmentResponse>()
.AssertThatResponse(r => r.Amount, Is.EqualTo(50));
// GET retries until condition is met or timeout expires
rest.Get("/inventory/stock")
.WithResponseType<StockResponse>()
.AssertThatResponse(r => r.Replenished, Is.True);
// POST executes once — assertion failure is immediate
rest.post("/shipment/accept")
.withRequestBody(() -> new AcceptShipmentRequest(50))
.withResponseType(AcceptShipmentResponse.class)
.assertThatResponse(r -> r.amount == 50);
// GET retries until condition is met or timeout expires
rest.get("/inventory/stock")
.withResponseType(StockResponse.class)
.assertThatResponse(r -> r.replenished);
Full JSON API support with typed response deserialization, bearer token authentication, and Promise<T> for passing data across steps. All HTTP verbs, query arguments, and custom assertions are included out of the box.
Promise<TokenResponse> token = rest.Post("/auth/login")
.WithRequestBody(() => new LoginRequest(username, password))
.WithResponseType<TokenResponse>()
.AssertThatResponse(r => r.AccessToken, Is.Not.Empty)
.PromiseResponse();
rest.Get("/api/profile")
.WithBearerTokenClient(() => token.Resolve().AccessToken)
.WithResponseType<ProfileResponse>()
.AssertThatResponse(r => r.Username, Is.EqualTo(username));
Promise<TokenResponse> token = rest.post("/auth/login")
.withRequestBody(() -> new LoginRequest(username, password))
.withResponseType(TokenResponse.class)
.assertThatResponse(r -> !r.accessToken.isEmpty())
.promiseResponse();
rest.get("/api/profile")
.withBearerTokenClient(() -> token.resolve().accessToken)
.withResponseType(ProfileResponse.class)
.assertThatResponse((ProfileResponse r) -> r.username.equals(username));
Each adapter instance carries its own cookie jar — session cookies persist automatically across steps. Test full login → authenticated page flows without any manual session management or cookie forwarding.
View docs →var ssr = builder.SsrAdapterBuilder()
.WithBaseUrl(scenario.GuiAddress).Build();
ssr.Post("/auth/register")
.WithFormContent(new RegisterRequest(username, password).ToForm())
.AssertSuccess();
ssr.Post("/auth/login")
.WithFormContent(new LoginRequest(username, password).ToForm())
.AssertSuccess();
// Session cookie carried automatically — no manual wiring
ssr.Get("/dashboard")
.AssertThatResponseContentString(Does.Contain(username));
var ssr = new SsrAdapterBuilder(builder)
.withBaseUrl(scenario.guiAddress).build();
ssr.post("/auth/register")
.withFormContent(SsrExtensions.toForm(
new RegisterRequest(username, password)))
.assertSuccess();
ssr.post("/auth/login")
.withFormContent(SsrExtensions.toForm(
new LoginRequest(username, password)))
.assertSuccess();
// Session cookie carried automatically — no manual wiring
ssr.get("/dashboard")
.assertThatResponseContentString(html -> html.contains(username));
Promise<T> bridges step registration time and execution time. Capture a typed response or HTML body from one step, then consume it lazily in the next — driving paths, extracting tokens, or feeding form values.
// A token is embedded in the rendered HTML page
Promise<string> page = ssr.Post("/token/create")
.WithFormContent(new TokenCreateRequest("deploy-key").ToForm())
.AssertSuccess()
.PromiseResponse();
// REST call resolves the token lazily at execution time
rest.Post("/api/env/create")
.WithBearerTokenClient(() => ExtractToken(page.Resolve()))
.AssertSuccess();
// A token is embedded in the rendered HTML page
Promise<String> page = ssr.post("/token/create")
.withFormContent(SsrExtensions.toForm(
new TokenCreateRequest("deploy-key")))
.assertSuccess()
.promiseResponse();
// REST call resolves the token lazily at execution time
rest.post("/api/env/create")
.withBearerTokenClient(() -> extractToken(page.resolve()))
.assertSuccess();
Adapters are plain objects — wired explicitly like any production dependency. That keeps execution order visible, data flow explicit, and coordination driven by conditions. Extend XceptoAdapter and XceptoState to create fluent chains that read as behavior, not infrastructure.
public sealed class OrderAdapter : XceptoAdapter
{
public OrderFlowBuilder Order(string orderId)
=> new OrderFlowBuilder(Builder).WithOrderId(orderId);
}
// In a test — reads as domain behavior
var orders = new OrderAdapterBuilder(builder).Build();
orders.Order("order-42")
.WithAmount(100)
.ShouldReachStatus(OrderStatus.Fulfilled);
public final class OrderAdapter extends XceptoAdapter {
public OrderFlowBuilder order(String orderId) {
return new OrderFlowBuilder(getBuilder())
.withOrderId(orderId);
}
}
// In a test — reads as domain behavior
var orders = new OrderAdapterBuilder(builder).build();
orders.order("order-42")
.withAmount(100)
.shouldReachStatus(OrderStatus.FULFILLED);