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.