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.