Fluent Builder
A fluent builder accumulates configuration from the test and builds a XceptoState when the chain is complete. It is registered as a future step in its constructor — the step is placed in the state machine immediately, and the builder fills in its configuration before the machine runs.
Extend AbstractStateBuilderIdentity
- .NET
- Java
public sealed class EntityFlowBuilder
: AbstractStateBuilderIdentity<EntityFlowBuilder>
{
private string? _entityId;
private DomainClient? _client;
private string? _displayName;
private EntityStatus? _expectedStatus;
public EntityFlowBuilder(IStateMachineBuilder builder) : base(builder) {}
protected override string DefaultName
=> $"Entity {_entityId ?? "?"} reaches {_expectedStatus}";
protected override bool DefaultRetry => true;
public EntityFlowBuilder WithEntityId(string id)
{
_entityId = id;
return this;
}
public EntityFlowBuilder WithClient(DomainClient client)
{
_client = client;
return this;
}
public EntityFlowBuilder WithDisplayName(string name)
{
_displayName = name;
return this;
}
public EntityFlowBuilder ShouldHaveStatus(EntityStatus status)
{
_expectedStatus = status;
return this;
}
protected override XceptoState Build()
=> new EntityStatusState(Name, _client!, _entityId!,
_displayName!, _expectedStatus!.Value);
}
For non-HTTP domain builders in Java, call transitionBuilder.addFutureStep(...) directly, or extend AbstractHttpStateBuilderIdentity<T> for HTTP-based adapters.
A simple non-HTTP builder:
public final class EntityFlowBuilder {
private final TransitionBuilder transitionBuilder;
private String entityId;
private DomainClient client;
private String displayName;
private EntityStatus expectedStatus;
public EntityFlowBuilder(TransitionBuilder tb) {
this.transitionBuilder = tb;
// Register a placeholder; will be replaced in build()
tb.addFutureStep(this::buildState, this);
}
public EntityFlowBuilder withEntityId(String id) {
this.entityId = id;
return this;
}
public EntityFlowBuilder withClient(DomainClient client) {
this.client = client;
return this;
}
public EntityFlowBuilder withDisplayName(String name) {
this.displayName = name;
return this;
}
public EntityFlowBuilder shouldHaveStatus(EntityStatus status) {
this.expectedStatus = status;
return this;
}
private XceptoState buildState() {
return new EntityStatusState(
"Entity " + entityId + " reaches " + expectedStatus,
client, entityId, expectedStatus);
}
}
HTTP-based builders
For custom HTTP adapters, extend AbstractHttpStateBuilderIdentity<T> to inherit the full shared fluent API (assertions, query arguments, retry control, URL building):
- .NET
- Java
public sealed class MyHttpBuilder
: AbstractRestStateBuilderIdentity<MyHttpBuilder>
{
private string? _tenantId;
public MyHttpBuilder(IStateMachineBuilder builder) : base(builder) {}
protected override string AdapterPrefix => "MyApi";
public MyHttpBuilder WithTenantId(string tenantId)
{
_tenantId = tenantId;
return this;
}
protected override XceptoState BuildRestState()
=> new MyHttpState(Name, UrlProducer, ClientProducer,
Verb, ResolveRetry(), Assertions, _tenantId);
}
public final class MyHttpBuilder
extends AbstractHttpStateBuilderIdentity<MyHttpBuilder> {
private String tenantId;
public MyHttpBuilder(TransitionBuilder tb) {
super(tb);
}
@Override protected MyHttpBuilder self() { return this; }
@Override protected String adapterPrefix() { return "MyApi"; }
public MyHttpBuilder withTenantId(String tenantId) {
this.tenantId = tenantId;
return this;
}
@Override
protected XceptoState buildState() {
return new MyHttpState(
resolveName(), buildUrlProducer(), clientProducer,
verb, resolveRetry(), new ArrayList<>(assertions),
tenantId);
}
}
Design rules
Every fluent method should add real value. Avoid no-argument methods that only make the chain read nicely. Each method should contribute data, select a target, choose an action, or declare an expectation.
Builders must not hide case data. Input values, entity identifiers, expected outcomes — these must come from the test through explicit fluent calls. A builder may define reusable domain operations and sensible defaults, but never hardcode test-case-specific values.
One state class per evaluation shape, not per test case. If two tests need the same kind of check with different parameters, parameterize the state through its constructor. See Custom State → State reuse.
Test usage
With the builder in place, test code reads as domain behavior:
- .NET
- Java
await XceptoTest.Given(new UserScenario(), builder =>
{
var users = new DomainAdapterBuilder(builder)
.WithClient(scenario.Client)
.Build();
users.Entity("entity-42")
.WithDisplayName("Operator")
.ShouldHaveStatus(EntityStatus.Active);
});
Xcepto.given(scenario, builder -> {
var users = new DomainAdapterBuilder(builder)
.withClient(scenario.client)
.build();
users.entity("entity-42")
.withDisplayName("Operator")
.shouldHaveStatus(EntityStatus.ACTIVE);
}, TIMEOUT, Duration.ofMillis(100));