Skip to main content

Anti-Patterns

These are the most common mistakes when writing Xcepto tests. Each one defeats the purpose of the framework.

Sleeping instead of declaring conditions​

❌ Thread.Sleep(3000) / Thread.sleep(3000)

Sleeps make tests slow, brittle, and environment-dependent. Replace them with an idempotent step (GET/PUT/DELETE) that retries until the condition is true.

// ❌ Sleep-based
rest.Post("/command").AssertSuccess();
await Task.Delay(3000);
rest.Get("/status").AssertThatResponseContentString(body => body.Contains("done"));

// ✅ Condition-based — Xcepto retries Get until the assertion passes
rest.Post("/command").AssertSuccess();
rest.Get("/status").AssertThatResponseContentString(body => body.Contains("done"));

Hiding test data in adapters​

Test-case-specific values (amounts, names, IDs, expected outcomes) must live in the test method, not inside adapter or state classes.

// ❌ Data hidden inside adapter method
warehouse.AcceptShipmentAndExpectReplenishment(); // what amount?

// ✅ Data supplied by the test
var amount = 50;
rest.Post("/shipment/accept")
.WithRequestBody(() => new AcceptShipmentRequest(amount))
.WithResponseType<AcceptShipmentResponse>()
.AssertThatResponse(r => r.Amount == amount);

Multiple cases in one test method​

One [Test] / @Test should cover exactly one behavior. If it internally tests multiple scenarios, a failure doesn't tell you which case failed.

❌ public async Task All_Shipment_Scenarios() { /* 5 cases inside */ }

✅ public async Task Valid_Shipment_IsAccepted()
✅ public async Task Duplicate_Shipment_IsRejected()
✅ public async Task Oversized_Shipment_IsRejected()

Testing implementation instead of behavior​

Test observable system behavior — what a user or API consumer sees. Do not test internal wiring.

❌ AssertThatResponseContentString(body => body.Contains("class=\"controller\""))
❌ AssertThatResponseContentString(body => body.Contains("[HttpPost]"))
❌ Checking that a route attribute exists on a controller action
❌ Verifying a private method is called

✅ ssr.Post("/auth/login").AssertSuccess()
✅ rest.Get("/user/profile").AssertThatResponseContentString(body => body.Contains(username))

Using Xcepto as a sequential assertion list​

If all your steps immediately transition without any retries or conditions, you're using Xcepto as a prettier Assert.That list. That is not wrong, but it misses the point: Xcepto's value is in expressing eventual behavior without sleeps or polling loops.

The correct signal: if your GET/PUT/DELETE assertions are always satisfied on the first evaluation, consider whether there is a real asynchronous expectation to model — and if so, where it belongs in the step chain.

Calling Promise.resolve() eagerly​

promise.resolve() must always be inside a lambda (supplier/func). Calling it directly at step-registration time will throw because the promise is not yet settled.

// ❌ Eager — throws at registration time
var body = htmlPromise.Resolve();
rest.Post("/api/action")
.WithBearerTokenClient(() => ExtractToken(body))

// ✅ Lazy — resolved at execution time
rest.Post("/api/action")
.WithBearerTokenClient(() => ExtractToken(htmlPromise.Resolve()))

A new state class per test case​

State classes represent reusable evaluation shapes, not specific test cases. If EntityActiveState, EntityInactiveState, and EntityPendingState differ only in the expected value, collapse them into one EntityStatusState(EntityStatus expected).

No negative-path test​

Every feature fixture should contain at least one negative-path test. An adapter or feature covered only by happy paths is not meaningfully tested — edge cases are where behavioral regressions hide.