xUnit’s design philosophy is opinionated — it enforces isolation by creating a new class instance per test and provides minimal setup/teardown options. This pushes test writers toward clean, isolated tests rather than shared mutable state. Understanding xUnit’s key features — Facts, Theories, Fixtures, and parallelisation — is essential for writing fast, reliable test suites for the BlogApp’s C# code.
xUnit Core Features
// ── [Theory] with multiple data sources ──────────────────────────────────
public class SlugGeneratorTests
{
private readonly SlugGenerator _sut = new();
// [InlineData] — simple inline values
[Theory]
[InlineData("Getting Started with .NET", "getting-started-with-net")]
[InlineData("C# vs F# vs VB", "c-vs-f-vs-vb")]
[InlineData(" Spaces Around ", "spaces-around")]
[InlineData("Hello World!", "hello-world")]
[InlineData("", "")] // edge case
public void Generate_ReturnsExpectedSlug(string title, string expected)
{
var result = _sut.Generate(title);
result.Should().Be(expected);
}
// [MemberData] — complex test data from a static property
public static IEnumerable<object[]> UnicodeTestCases =>
new List<object[]>
{
new object[] { "Über alles", "uber-alles" },
new object[] { "日本語", "" }, // non-Latin stripped
new object[] { "café au lait", "cafe-au-lait" },
};
[Theory]
[MemberData(nameof(UnicodeTestCases))]
public void Generate_WithUnicodeInput_NormalisesCorrectly(
string title, string expected)
{
_sut.Generate(title).Should().Be(expected);
}
}
// ── IClassFixture — expensive shared setup per test class ─────────────────
// Database setup that should run once per class, not per test:
public class PostsRepositoryTests : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _db;
public PostsRepositoryTests(DatabaseFixture db) => _db = db;
[Fact]
public async Task GetBySlugAsync_WhenSlugExists_ReturnsPost()
{
var post = await _db.Repository.GetBySlugAsync("existing-slug", default);
post.Should().NotBeNull();
post!.Slug.Should().Be("existing-slug");
}
}
// ── ICollectionFixture — share one fixture across multiple test classes ────
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
[Collection("Database")]
public class CommentsRepositoryTests
{
private readonly DatabaseFixture _db;
public CommentsRepositoryTests(DatabaseFixture db) => _db = db;
// ...
}
// ── ITestOutputHelper — diagnostic output in test results ─────────────────
public class PerformanceTests
{
private readonly ITestOutputHelper _output;
public PerformanceTests(ITestOutputHelper output) => _output = output;
[Fact]
public void GetPublished_ShouldCompleteUnder100ms()
{
var sw = Stopwatch.StartNew();
// ... test code ...
sw.Stop();
_output.WriteLine($"Elapsed: {sw.ElapsedMilliseconds}ms");
sw.ElapsedMilliseconds.Should().BeLessThan(100);
}
}
// ── [Trait] — categorise tests for selective running ──────────────────────
[Trait("Category", "Unit")]
public class PostsServiceTests { /* ... */ }
[Trait("Category", "Integration")]
public class PostsControllerIntegrationTests { /* ... */ }
// Run only unit tests:
// dotnet test --filter "Category=Unit"
[Collection("DatabaseTests")] to disable parallelism between specific test classes that share a resource.[Theory] with [InlineData] for any test that validates a function against multiple inputs — slug generation, email validation, status transitions, price calculation. A single [Theory] with 20 data points is cleaner and faster to add to than 20 separate [Fact] methods. The xUnit test runner reports each data point as a separate test result, so failures are pinpointed to the exact failing input.IClassFixture<T> creates the fixture once per test class and shares it across all test methods in that class. If tests modify the fixture’s state (e.g., inserting rows into a shared test database), earlier tests can affect later tests — creating order-dependent, flaky tests. Design class fixtures as read-only where possible, or reset state between tests using database transactions that are rolled back after each test.Common Mistakes
Mistake 1 — Static shared state between xUnit test classes (test interference)
❌ Wrong — static field holding test data modified by one test class; second parallel test class reads stale/modified data.
✅ Correct — no shared mutable static state; use IClassFixture for expensive setup; create fresh instances per test method.
Mistake 2 — [InlineData] with magic values (unclear test intent)
❌ Wrong — [InlineData("a", true)]; what does “a” represent? Why should it be true?
✅ Correct — [InlineData("valid-slug", true)]; the value communicates its intent directly.