Custom State
A XceptoState is a single step in the state machine. It has two entry points that serve distinct roles:
| Method | When called | Purpose |
|---|---|---|
onEnter / OnEnter | Once, when the machine enters this state | One-time actions (commands, mutations) |
evaluateConditionsForTransition / EvaluateConditionsForTransition | Repeatedly, until it returns true | Conditions (expectations, readiness checks) |
Action state (execute once)
Use this for commands that should run exactly once and fail immediately if they don't succeed:
- .NET
- Java
public sealed class SendCommandState : XceptoState
{
private readonly DomainClient _client;
private readonly string _entityId;
private readonly DomainAction _action;
public SendCommandState(string name, DomainClient client,
string entityId, DomainAction action) : base(name)
{
_client = client;
_entityId = entityId;
_action = action;
}
public override async Task OnEnter(IServiceProvider sp)
{
var result = await _client.SendCommandAsync(_entityId, _action);
if (!result.Accepted)
throw new XceptoTestFailedException(
$"Command {_action} was not accepted: {result.Reason}");
}
public override Task<bool> EvaluateConditionsForTransition(
IServiceProvider sp) => Task.FromResult(true);
}
public final class SendCommandState extends XceptoState {
private final DomainClient client;
private final String entityId;
private final DomainAction action;
public SendCommandState(String name, DomainClient client,
String entityId, DomainAction action) {
super(name);
this.client = client;
this.entityId = entityId;
this.action = action;
}
@Override
public void onEnter(ServiceProvider sp) throws XceptoTestFailedException {
var result = client.sendCommand(entityId, action);
if (!result.accepted())
throw new XceptoTestFailedException(
"Command " + action + " was not accepted: " + result.reason());
}
@Override
public boolean evaluateConditionsForTransition(ServiceProvider sp) {
return true;
}
}
Expectation state (retry until matched)
Use this for conditions that may not be satisfied immediately — the machine retries until they pass:
- .NET
- Java
public sealed class EntityStatusState : XceptoState
{
private readonly DomainClient _client;
private readonly string _entityId;
private readonly EntityStatus _expected;
public EntityStatusState(string name, DomainClient client,
string entityId, EntityStatus expected) : base(name)
{
_client = client;
_entityId = entityId;
_expected = expected;
}
public override Task OnEnter(IServiceProvider sp) => Task.CompletedTask;
public override async Task<bool> EvaluateConditionsForTransition(
IServiceProvider sp)
{
var actual = await _client.GetStatusAsync(_entityId);
var matched = actual == _expected;
if (!matched)
MostRecentFailingResult = new ConditionResult(
actual,
$"Expected {_entityId} to reach {_expected}, got {actual}");
return matched;
}
}
public final class EntityStatusState extends XceptoState {
private final DomainClient client;
private final String entityId;
private final EntityStatus expected;
public EntityStatusState(String name, DomainClient client,
String entityId, EntityStatus expected) {
super(name);
this.client = client;
this.entityId = entityId;
this.expected = expected;
}
@Override
public void onEnter(ServiceProvider sp) {}
@Override
public boolean evaluateConditionsForTransition(ServiceProvider sp)
throws XceptoTestFailedException {
var actual = client.getStatus(entityId);
return actual == expected;
}
}
MostRecentFailingResult
When evaluateConditionsForTransition returns false and the test eventually times out, the timeout error includes the state's name. Use MostRecentFailingResult (.NET) to surface diagnostic information about the last failing evaluation:
MostRecentFailingResult = new ConditionResult(
actual,
$"Expected {_entityId} to reach {_expected}, got {actual}");
This appears in the timeout exception message, making it clear why the state failed to transition — not just which state was stuck.
State name matters
The state name is the primary diagnostic output when a test times out. Use descriptive names:
- .NET
- Java
// Good — explains what was expected
new EntityStatusState($"Entity {entityId} reaches {expected}", ...)
// Bad — generic, unhelpful on timeout
new EntityStatusState("Assertion", ...)
// Good
new EntityStatusState("Entity " + entityId + " reaches " + expected, ...)
// Bad
new EntityStatusState("Assertion", ...)
State reuse
A state class should represent a reusable evaluation shape — not a specific test case. Push test-case-specific data (entity IDs, expected values, action types) into constructor parameters and expose them through the builder.
Do not create a new state class for each test case. See Fluent Builder for how builders parameterize states.