Skip to main content

Mental Model

Tests as state machines​

Test steps are not executed immediately when declared. They are compiled into a state machine where each transition depends on the specified conditions.

The states are linked in a chain:

[Start] → [First] → [Second] → [Third] → [Final]

The test passes if the Final state is reached before the timeout expires. There is no explicit assertion at the end — the machine reaching its final state is the assertion.

Causality, not time​

The central insight behind Xcepto is the difference between waiting for time and waiting for causality.

Arrange–Act–Assert implicitly assumes synchronicity: the assertion happens after the action, as if the system has already settled. But distributed systems don't work like that. An API call may publish an event, a queue consumer processes it later, a workflow eventually converges — timing is unpredictable.

The common workaround is Thread.Sleep or manual polling loops. These make tests brittle and environment-dependent: too short and the test flickers; too long and the suite slows down.

Xcepto replaces timing assumptions with explicit conditions. A step does not advance because enough time has passed — it advances because the expected system state has been observed. Slower environment? The test still passes. Faster environment? No time is wasted waiting.

Given-When-Then alignment​

Given-When-Then is not just syntax — it is the right mental model for asynchronous systems.

Xcepto aligns naturally with Given-When-Then semantics:

BDD conceptXcepto concept
GivenScenario — environment setup and teardown
WhenNon-idempotent steps (POST, PATCH) — actions that change state
ThenIdempotent steps (GET, PUT, DELETE) — conditions that wait for observable state

Because each validated state becomes the starting point for the next transition, you can chain When → Then pairs together naturally. The test describes a causal sequence — not a timing sequence.

Retry vs execute-once​

The framework distinguishes between two step types based on HTTP verb idempotency:

VerbBehaviorFailure mode
GET, PUT, DELETERetried — evaluates evaluateConditionsForTransition repeatedlyTimes out if assertion never passes
POST, PATCHExecutes once — runs in onEnter, fails immediately on assertion failureFails immediately

This models the natural semantics of distributed systems: you issue a command once (POST) and then poll for the resulting state (GET) until it reflects the change.

Step lifecycle​

Each step is an XceptoState with two entry points:

  • onEnter — runs once when the state is entered. Used for actions (POST/PATCH) and one-time setup.
  • evaluateConditionsForTransition — polled repeatedly. Returns true when the machine should advance. Used for expectations (GET/PUT/DELETE).

The framework handles the polling loop, timeout enforcement, and cleanup. You only describe what the condition is — not how long to wait.

Example flow​

POST /shipment/accept  →  [onEnter: send request, assert response]
GET /inventory/stock → [evaluateConditionsForTransition: check body, retry until match]

If the GET never returns the expected body, the test times out with a clear location: the state that failed to transition.