Best Practices
Docs for xUnit SDK v0.4.0
This guide covers the conventions and patterns that make LiveDoc xUnit tests readable, maintainable, and useful as living documentation. Follow these practices to get the most out of the framework.
Namespace Organization for Report Hierarchy
The full declared C# namespace of each test class determines its position in the LiveDoc Viewer tree. Every namespace segment is retained, independently of the assembly name, and becomes part of the folder-like hierarchy. Put Features and Specifications for the same capability together; the pattern is not an audience restriction.
Recommended Structure
MyApp
└── Tests
├── Orders
│ ├── CheckoutFeature.cs
│ └── Pricing
│ ├── ShippingRatesSpecification.cs
│ └── DiscountsSpecification.cs
└── Accounts
└── RegistrationFeature.cs
Declare MyApp.Tests.Orders for CheckoutFeature and
MyApp.Tests.Orders.Pricing for both pricing Specifications. This puts
Checkout and its detailed pricing rules under MyApp → Tests → Orders in
the Viewer, whether the assembly is named MyApp.Tests or something else.
Class names form .cs document leaves; class names and Feature/Specification
titles do not introduce additional navigation folders. A class with no
namespace remains a document at the root.
Physical folders alone do not set the report path; a flat MyApp.Tests namespace
would produce a flat list, while MyApp.Tests.Features.Orders and
MyApp.Tests.Specifications.Orders would split Orders in two.
Align source folders and namespaces with business capabilities. A product manager can read an Orders Feature and then inspect the associated Specification's precise rules without leaving Orders. See Test Organization.
Self-Documenting Test Titles
The core principle of living documentation: all test inputs and expected outputs must be visible in step titles. A reader should understand what a test does without reading the implementation.
// ✅ GOOD: Values in titles, extracted via context
[Scenario]
public void Free_shipping_for_orders_over_100()
{
Given("the customer is from 'Australia'", ctx =>
{
_cart.Country = ctx.Step!.Values[0].AsString();
});
When("the order totals '100.00' dollars", ctx =>
{
_cart.Total = ctx.Step!.Values[0].AsDecimal();
_cart.Calculate();
});
Then("shipping type is 'Free'", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsString(), _cart.ShippingType);
});
}
// ❌ BAD: Values hidden inside code — not living documentation
[Scenario]
public void Free_shipping()
{
Given("the customer is from their country", () =>
{
_cart.Country = "Australia"; // What country? Title doesn't say
});
Then("shipping is correct", () =>
{
Assert.Equal("Free", _cart.ShippingType); // What's "correct"?
});
}
Keep One Primary Given, When, and Then
A Scenario should have one primary precondition, one primary action, and one
primary outcome. Use And or But to continue the current phase:
Given("an authenticated administrator", () => AuthenticateAdministrator());
And("an export containing '4' configuration sections", ctx =>
LoadExport(ctx.Step!.Values[0].AsInt()));
When("the export is imported", () => ImportConfiguration());
Then("all '4' sections are restored", ctx =>
Assert.Equal(ctx.Step!.Values[0].AsInt(), RestoredSectionCount));
And("the import is recorded in the audit log", () =>
Assert.True(AuditLogContainsImport()));
LiveDoc reports repeated or missing Given/When/Then calls, untitled steps, and And/But calls without a preceding primary step as non-fatal rule violations. They appear in the Viewer without changing test pass/fail status.
Explain Purpose in Descriptions
Prefer Description on container attributes ([Feature],
[Specification]) when it gives readers a reason to care; the property
remains optional. Describe the purpose within the behavior the tests
actually prove, not the list of branches, endpoints, or tests. Titles,
steps, Rules, and assertions carry the technical proof. Optional Scenario or
Rule descriptions can clarify scope without inflating the claim.
[Feature("Shopping Cart", Description = @"
Keeps checkout totals predictable for the cart
and shipping cases covered below.")]
public class CartTests : FeatureTest
{
public CartTests(ITestOutputHelper output) : base(output) { }
[Scenario(Description = "Covers the Australian shipping threshold; other destinations are separate cases.")]
public void Free_shipping_in_Australia() { ... }
}
[Specification("Email Validation", Description = @"
Helps callers reject malformed addresses before
using them in the flows covered by these rules.")]
public class EmailSpec : SpecificationTest
{
public EmailSpec(ITestOutputHelper output) : base(output) { }
}
Choosing BDD vs. Specification
LiveDoc supports two patterns. Choose based on how best to prove the claim, not on whether a product manager or developer wants to read it:
| Aspect | BDD / Gherkin | Specification / MSpec |
|---|---|---|
| Base class | FeatureTest | SpecificationTest |
| Attributes | [Feature], [Scenario] | [Specification], [Rule] |
| Reader's focus | Following a workflow | Inspecting exact rules and examples |
| Best for | User journeys, acceptance tests | Domain policies, contracts, edge cases |
| Verbosity | Higher (Given/When/Then steps) | Lower (direct assertions) |
| Data-driven | [ScenarioOutline] + [Example] | [RuleOutline] + [Example] |
Use BDD when a workflow benefits from a narrative
[Feature("User Registration")]
public class RegistrationTests : FeatureTest
{
[Scenario]
public void Successful_registration()
{
Given("a new user with email 'alice@example.com'", ctx => { ... });
When("they submit the registration form", () => { ... });
Then("the account is created", () => { ... });
And("a welcome email is sent to 'alice@example.com'", ctx => { ... });
}
}
Use Specification when direct rules communicate more clearly
[Specification("Calculator Operations", Description = "Callers can rely on the arithmetic results covered by these rules.")]
public class CalculatorSpec : SpecificationTest
{
public CalculatorSpec(ITestOutputHelper output) : base(output) { }
[Rule("Adding '5' and '3' returns '8'")]
public void Addition()
{
var (a, b, expected) = Rule.Values.As<int, int, int>();
Assert.Equal(expected, a + b);
}
[RuleOutline("Dividing '<a>' by '<b>' returns '<result>'")]
[Example(10, 2, 5)]
[Example(100, 10, 10)]
public void Division(int a, int b, int result)
{
Assert.Equal(result, a / b);
}
}
You can mix both patterns in the same project. Use [Feature] for
acceptance tests and [Specification] for unit/component tests.
Value Extraction Over Hardcoding
Never hardcode values inside step implementations that are already present in the title. Always extract them using the context APIs to prevent value drift — where the title says one thing but the code tests another.
// ✅ CORRECT: Values extracted from context
Then("the balance should be '300' dollars", ctx =>
{
Assert.Equal(ctx.Step!.Values[0].AsDecimal(), _account.Balance);
});
// ✅ ALSO CORRECT: Named parameters
Then("the balance should be <expected:300> dollars", ctx =>
{
Assert.Equal(ctx.Step!.Params["expected"].AsDecimal(), _account.Balance);
});
// ❌ WRONG: Value drift risk — title says 300, code checks 200
Then("the balance should be '300' dollars", ctx =>
{
Assert.Equal(200m, _account.Balance); // BUG: title and code disagree
});
For ScenarioOutline and RuleOutline, use method parameters — they're
automatically injected from [Example] data:
[ScenarioOutline]
[Example("Australia", 100.00, "Free")]
public void Shipping(string country, decimal total, string expected)
{
// ✅ Parameters are typed and injected — no manual extraction needed
Then("shipping type is <expected>", () =>
{
Assert.Equal(expected, _cart.ShippingType);
});
}
Test Isolation
Each xUnit test class is instantiated per test method. Use the constructor for shared setup and ensure each test is independent:
[Feature("Order Processing")]
public class OrderTests : FeatureTest
{
private readonly OrderService _service;
private Order _order = null!;
public OrderTests(ITestOutputHelper output) : base(output)
{
// Runs before each Scenario — fresh state every time
_service = new OrderService();
}
[Scenario]
public void Create_new_order()
{
Given("a valid customer", () => { ... });
When("they place an order", () =>
{
_order = _service.CreateOrder(...);
});
Then("the order status is 'Pending'", ctx => { ... });
}
}
Avoid shared mutable state across test methods. xUnit creates a new class instance per test, but shared static fields or singletons can leak state between tests.
One Independent Claim Per Rule
In the Specification pattern, each [Rule] should report one independent
contract claim. Multiple assertions are appropriate when they jointly prove
that claim; unrelated input cases deserve separate Rules or an outline row
each. That keeps failures specific without forcing one assertion per method.
// ✅ GOOD: One claim whose amount and currency both matter
[Rule("Parsing 'USD 12.50' yields currency 'USD' and amount '12.50'")]
public void Parse_money()
{
var (input, currency, amount) = Rule.Values.As<string, string, decimal>();
var parsed = ParseMoney(input);
Assert.Equal(currency, parsed.Currency);
Assert.Equal(amount, parsed.Amount);
}
// ❌ BAD: Independent cases collapsed into one reported result
[Rule("Addition works correctly")]
public void Addition()
{
Assert.Equal(5, 2 + 3);
Assert.Equal(0, 0 + 0);
Assert.Equal(-1, -3 + 2);
}
For separate cases, use [RuleOutline("Adding '<a>' and '<b>' returns '<expected>'")] and a distinct [Example] for each input pair.
One Feature Per Class
Each test class should represent a single feature or specification. This keeps files focused and maps cleanly to the viewer's tree structure.
// ✅ GOOD: One feature per class
[Feature("Shopping Cart", Description = "Keeps cart contents consistent when items are added or removed.")]
public class ShoppingCartTests : FeatureTest { ... }
[Feature("Checkout", Description = "Keeps checkout totals consistent with cart contents and discounts.")]
public class CheckoutTests : FeatureTest { ... }
// ❌ BAD: Multiple features crammed into one class
[Feature("Shopping")] // Too broad — what aspect of shopping?
public class ShoppingTests : FeatureTest
{
// Mixing cart, checkout, and shipping scenarios in one class
}
Summary Checklist
- Namespaces mirror domain boundaries for clean viewer hierarchy
- Step titles contain all inputs and expected outputs
- Gherkin structure uses one primary Given, When, and Then with And/But continuations
- Descriptions, when provided, explain purpose without claiming untested outcomes
- Values extracted via
ctx.Step.Valuesorctx.Step.Params— never hardcoded - Constructor accepts
ITestOutputHelperand passes tobase(output) - One feature/specification per class
- One independent claim per
[Rule]; related assertions may prove it together - Pattern chosen appropriately: BDD for stakeholders, Specification for developers
Related
- Value Extraction API — full reference for
ValuesandParams - Attributes — complete attribute reference
- Your First Feature — step-by-step BDD tutorial
- Your First Specification — step-by-step MSpec tutorial