Skip to main content

Test Structure

One behavior per test

Each @Test / [Test] method should verify exactly one behavior. A clear, focused test:

  • Has a name that reads as a sentence describing the expected outcome
  • Declares all test-case-specific data as local variables
  • Contains one happy or one negative path — not both
✅ shipmentAccepted_stockIsReplenished
✅ invalidCredentials_loginIsRejected
❌ shipmentTest (vague — what's being tested?)
❌ testAllShipmentScenarios (combines multiple cases)

Happy path + negative path

Every feature should have at least one happy-path test and one negative-path test in the same fixture. Both live in the same class:

[TestFixture]
public sealed class UserRegistrationTests
{
private static readonly TimeoutConfig Timeout =
new TimeoutConfig(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5));

[Test]
public async Task NewUser_CanRegister()
{
var username = "[email protected]";
var password = "ValidPass1!";

await XceptoTest.Given(new UserScenario(), Timeout, builder =>
{
var ssr = builder.SsrAdapterBuilder()
.WithBaseUrl(new Uri("http://localhost:8080"))
.Build();

ssr.Post("/auth/register")
.WithFormContent(
new RegisterRequest(username, password).ToForm())
.AssertSuccess()
.AssertThatResponseContentString(
Does.Not.Contain("id=\"errors\""));
});
}

[Test]
public void WeakPassword_IsRejected()
{
var username = "[email protected]";
var weakPassword = "abc";

Assert.ThrowsAsync<XceptoTestFailedException>(() =>
XceptoTest.Given(new UserScenario(), Timeout, builder =>
{
var ssr = builder.SsrAdapterBuilder()
.WithBaseUrl(new Uri("http://localhost:8080"))
.Build();

ssr.Post("/auth/register")
.WithFormContent(
new RegisterRequest(username, weakPassword).ToForm())
.AssertSuccess()
.AssertThatResponseContentString(
Does.Not.Contain("id=\"errors\""));
}));
}
}

Folder / package structure

Organize by feature area, not by implementation layer:

tests/
├── UserRegistration/
│ ├── UserRegistrationTests.cs (.NET) / tests/UserRegistrationTests.java (Java)
│ └── UserLoginTests.cs / tests/UserLoginTests.java
├── ShipmentAcceptance/
│ └── ShipmentAcceptanceTests.cs / tests/ShipmentAcceptanceTests.java
├── Adapters/ — custom adapter classes
├── Builders/ — fluent builder classes
├── Scenarios/ — scenario classes
└── States/ — custom state classes
  • Name feature folders/packages after the capability, not after "Given", "When", "Then", or "Tests"
  • Split a fixture before it grows beyond ~5 real test methods
  • Do not create a single catch-all file for a whole component

Keep test data in the test

All case-specific data — entity IDs, usernames, passwords, amounts, expected values — belongs inside the @Test / [Test] method as local variables. Pass it into adapter calls through explicit fluent methods.

Never put test data in the scenario, adapter, or state class. These should be reusable across many test cases.

// ✅ Good — data is local and explicit
[Test]
public async Task Accepted_Shipment_ReplenishesStock()
{
var amount = 50;
var expectedStock = 50;

await XceptoTest.Given(new WarehouseScenario(), builder =>
{
var rest = builder.RestAdapterBuilder()
.WithBaseUrl(...)
.Build();

rest.Post("/shipment/accept")
.WithRequestBody(() => new AcceptShipmentRequest(amount))
.WithResponseType<AcceptShipmentResponse>()
.AssertThatResponse(r => r.Amount == amount);

rest.Get("/inventory/stock")
.AssertThatResponseContentString(
body => body.Contains($"\"stock\":{expectedStock}"));
});
}
// ❌ Bad — data hidden in the adapter
[Test]
public async Task Stock_IsReplenished()
{
await XceptoTest.Given(new WarehouseScenario(), builder =>
{
var warehouse = new WarehouseAdapterBuilder(builder).Build();
warehouse.AcceptDefaultShipmentAndCheckStock(); // What amount? What stock?
});
}