| description | Guides the agent in writing, naming, and organizing xUnit tests for .NET class libraries. Use this skill when asked to add or refactor tests, write unit or integration tests, set up fixtures or traits, organize test data with Bogus, or apply AAA structure and naming conventions to test methods. |
| license | MIT |
| metadata | {"author":"Antonello Provenzano","compatibility":["github-copilot","claude-code","openai-codex"],"github-path":"plugins/dotnet-tests/skills/xunit-test-organization","github-ref":"refs/heads/main","github-repo":"https://github.com/deveel/agents-skills","github-tree-sha":"4709dc809d2e833a753844334fe7512682c5a4de","version":"1.0"} |
| name | xunit-test-organization |
xUnit Test Organization
This skill covers the coding-level conventions for xUnit tests in .NET —
test method naming, class structure, test data generation with Bogus, fixture
patterns, trait categorization, and integration test rules. For project layout,
MSBuild configuration, and package selection see the xunit-test-arch skill
(in the dotnet-arch plugin).
1. Test Naming Convention
All test methods follow the pattern:
Should_{ExpectedResult}_When_{Scenario}
Examples:
Should_ReturnNull_When_InputIsEmpty()
Should_ThrowArgumentException_When_ValueIsNegative()
Should_ParseCorrectly_When_ValidJsonIsProvided()
Should_ReturnCachedResult_When_CalledTwice()
Rules:
- Use PascalCase for each segment
ExpectedResult describes the observable outcome
Scenario describes the condition or input state
- Never abbreviate — clarity is more important than brevity
- Test class name:
{ClassName}Tests (e.g. OrderServiceTests) — note the
class suffix remains Tests even though the project suffix is .XUnit
2. Test Class Structure
Each test class must follow this internal layout order:
namespace MyLib.XUnit.Unit;
public class OrderServiceTests : IClassFixture<OrderServiceFixture>
{
private readonly OrderServiceFixture _fixture;
public OrderServiceTests(OrderServiceFixture fixture)
{
_fixture = fixture;
}
#region ProcessOrder
[Fact]
public void Should_ReturnConfirmed_When_OrderIsValid()
{
var order = _fixture.BuildValidOrder();
var result = _fixture.Sut.ProcessOrder(order);
Assert.Equal(OrderStatus.Confirmed, result.Status);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void Should_ThrowArgumentException_When_QuantityIsNotPositive(int quantity)
{
var order = _fixture.BuildOrderWithQuantity(quantity);
var ex = Assert.Throws<ArgumentException>(
() => _fixture.Sut.ProcessOrder(order));
Assert.Contains("quantity", ex.Message);
}
#endregion
}
For xUnit v3 async tests that call async APIs with a CancellationToken
parameter, always flow the test runner token into those calls:
[Fact]
public async Task Should_ReturnOrder_When_OrderExistsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var order = _fixture.BuildValidOrder();
await _fixture.Sut.SaveAsync(order, cancellationToken);
var result = await _fixture.Sut.GetByIdAsync(order.Id, cancellationToken);
Assert.NotNull(result);
Assert.Equal(order.Id, result.Id);
}
Rules:
- Always use
// Arrange, // Act, // Assert comments — no exceptions
- One assertion concept per
[Fact]; multiple related assertions are allowed
within the same concept (e.g. asserting multiple properties of the same result)
[Theory] is required whenever the same logic is tested with multiple inputs;
never duplicate [Fact] methods with hardcoded variations
- Use
[InlineData] for simple scalar values; use [MemberData] pointing to a
public static IEnumerable<object[]> property for complex objects
- In xUnit v3 async
[Fact] / [Theory] methods, when the async API under test
accepts a CancellationToken, pass TestContext.Current.CancellationToken
through every awaited call so cancelled test runs do not remain blocked waiting
on the underlying operation
3. Test Data — Bogus & Randomization
All test data must be generated using Bogus. Never use hardcoded literals
for names, emails, addresses, IDs, amounts, or any domain value that could
realistically vary. Randomized data catches edge cases that fixed values miss
and prevents tests from accidentally passing due to magic constants.
Key rules:
- Define one
Faker<T> per entity type as a static readonly field in the
fixture — never instantiate Faker<T> inside individual test methods
- Always use explicit
RuleFor for every property; never rely on Bogus
auto-generation without rules (it can produce unexpected nulls)
- Use
f.Random.Guid() instead of Guid.NewGuid() so randomization flows
through the Bogus seed; use locale "en" when string format matters
- Use a fixed seed (
UseSeed(n)) only in [MemberData] datasets or for
regression reproduction — elsewhere let Bogus randomize freely
- Assert on observable behaviour / shape for freely randomized values;
use exact-value assertions only when the value is deterministic (seeded
or explicitly overridden)
→ See references/bogus.md for full placement
patterns, seeding guidance, [MemberData] examples, and assertion strategy.
4. Fixtures
Fixtures that are reused across multiple test projects belong in the shared
.Testing project (e.g. MyLib.Testing). Fixtures used by a single test
project can live in its Fixtures/ subfolder directly.
ClassFixture — shared across all tests in one class
Use when the system under test (SUT) is expensive to construct and is stateless
between tests, or when its state is always reset in the fixture constructor.
namespace MyLib.Testing.Fixtures;
public class OrderServiceFixture
{
private static readonly Faker<Order> OrderFaker = new Faker<Order>("en")
.RuleFor(o => o.Id, f => f.Random.Guid())
.RuleFor(o => o.Quantity, f => f.Random.Int(1, 100))
;
public OrderService Sut { get; }
public OrderServiceFixture()
{
var repo = new InMemoryOrderRepository();
Sut = new OrderService(repo);
}
public Order BuildValidOrder() => OrderFaker.Generate();
public Order BuildOrderWithQuantity(int quantity) =>
OrderFaker.Clone().RuleFor(o => o.Quantity, quantity).Generate();
}
CollectionFixture — shared across multiple test classes
Use for expensive shared infrastructure (e.g. a single in-memory database or
HTTP server) that must be shared across more than one test class.
namespace MyLib.Testing.Fixtures;
public class DatabaseFixture : IAsyncLifetime
{
public IDbConnection Connection { get; private set; } = default!;
public async Task InitializeAsync()
{
Connection = new SqliteConnection("Data Source=:memory:");
await Connection.OpenAsync();
}
public async Task DisposeAsync() => await Connection.DisposeAsync();
}
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
Apply to test classes with:
[Collection("Database")]
public class OrderRepositoryTests
{
private readonly DatabaseFixture _db;
public OrderRepositoryTests(DatabaseFixture db) => _db = db;
}
5. Traits — Categorization
All tests must be decorated with [Trait] to allow selective test runs in CI.
Standard trait keys
| Key | Values |
|---|
Category | Unit, Integration |
Layer | Domain, Application, Infrastructure |
Feature | The feature or module name (e.g. OrderProcessing) |
Examples:
[Fact]
[Trait("Category", "Unit")]
[Trait("Layer", "Domain")]
[Trait("Feature", "OrderProcessing")]
public void Should_ReturnConfirmed_When_OrderIsValid() { ... }
[Fact]
[Trait("Category", "Integration")]
[Trait("Layer", "Infrastructure")]
[Trait("Feature", "OrderProcessing")]
public void Should_PersistOrder_When_ProcessingSucceeds() { ... }
To run only unit tests in CI:
dotnet test -- --filter "Trait[Category]=Unit"
dotnet test -- --filter "Trait[Category]=Integration"
dotnet test -- --filter "Trait[Feature]=OrderProcessing"
dotnet test --filter "Category=Unit"
dotnet test --filter "Category=Integration"
6. Integration Tests
Integration tests live in the Integration/ subfolder and follow the same
naming and structure conventions as unit tests, with these additional rules:
- Class must be decorated with
[Collection("...")] referencing a
CollectionFixture that owns the shared infrastructure
- Tests must be fully isolated — always clean up or reset state in the fixture's
InitializeAsync / DisposeAsync
- No mocking of infrastructure in integration tests; use real implementations
or in-memory equivalents (e.g.
SqliteConnection, WebApplicationFactory)
- Use
IAsyncLifetime on fixtures that manage async resources
[Collection("Database")]
[Trait("Category", "Integration")]
[Trait("Layer", "Infrastructure")]
[Trait("Feature", "OrderRepository")]
public class OrderRepositoryIntegrationTests
{
private readonly DatabaseFixture _db;
public OrderRepositoryIntegrationTests(DatabaseFixture db) => _db = db;
[Fact]
public async Task Should_PersistOrder_When_SaveAsyncIsCalled()
{
var cancellationToken = TestContext.Current.CancellationToken;
var repo = new OrderRepository(_db.Connection);
var order = _db.OrderFaker.Generate();
await repo.SaveAsync(order, cancellationToken);
var retrieved = await repo.GetByIdAsync(order.Id, cancellationToken);
Assert.NotNull(retrieved);
Assert.Equal(order.Quantity, retrieved.Quantity);
}
}
7. What the Agent Must Never Do
- Do not use
[Fact] where [Theory] is more appropriate
- Do not write test methods without all three AAA comment sections
- Do not use FluentAssertions — always use xUnit's built-in
Assert class
- Do not share mutable state between tests without a fixture
- Do not name test methods with generic names like
Test1, TestProcess, etc.
- Do not skip adding
[Trait] attributes
- Do not hardcode test data values (names, IDs, quantities, emails, etc.) — always use Bogus
- Do not instantiate
Faker<T> inside individual test methods — define it in the fixture
- Do not use brittle exact-value assertions against non-deterministic
Bogus-generated values; use exact assertions when values are intentionally
deterministic (for example seeded or explicitly overridden)
- In xUnit v3 async tests, do not omit
TestContext.Current.CancellationToken
when calling async APIs that already expose a CancellationToken parameter
8. Local References
Additional supporting material for this skill is available in the
references/README.md index beside this file.
Use these files when deeper background or authoritative external links are
helpful, while treating this SKILL.md as the primary instruction source.