xUnit — Test Structure & Lifecycle
xUnit is the default test framework for ASP.NET Core. Unlike NUnit and MSTest, xUnit creates a new class instance per test (no shared state), discourages setup/teardown via IAsyncLifetime, and supports parallelism by default — making it ideal for fast, reliable test suites.
Facts, Theories & Test Organization
using Xunit;
using FluentAssertions;
// ━━ [Fact] — single test case ━━
public class SlugGeneratorTests
{
private readonly SlugGenerator _sut; // sut = System Under Test
public SlugGeneratorTests() =>
_sut = new SlugGenerator(); // fresh instance per test — xUnit's design
[Fact]
public void GenerateSlug_WithSimpleText_ReturnsLowercase()
{
// Arrange
var input = "Hello World";
// Act
var slug = _sut.Generate(input);
// Assert — FluentAssertions: readable, produces great error messages
slug.Should().Be("hello-world");
}
[Fact]
public void GenerateSlug_WithSpecialChars_RemovesNonAlphanumeric()
{
var slug = _sut.Generate("C# & .NET Development");
slug.Should().Be("c-net-development");
slug.Should().NotContain("#").And.NotContain("&").And.NotContain(" ");
}
// ━━ [Theory] + [InlineData] — parametric tests ━━
[Theory]
[InlineData("Hello World", "hello-world")]
[InlineData("C# Programming", "c-programming")]
[InlineData(" leading spaces", "leading-spaces")]
[InlineData("multiple spaces","multiple-spaces")]
[InlineData("UPPER CASE", "upper-case")]
public void GenerateSlug_VariousInputs_ReturnsExpectedSlug(string input, string expected)
{
_sut.Generate(input).Should().Be(expected);
}
// ━━ [Theory] + [MemberData] — complex data from static method ━━
public static IEnumerable<object[]> SlugEdgeCases() =>
[
[string.Empty, ""],
[" ", ""],
["a", "a"],
[new string('a', 300), new string('a', 250)], // truncation at 250 chars
];
[Theory, MemberData(nameof(SlugEdgeCases))]
public void GenerateSlug_EdgeCases(string input, string expected) =>
_sut.Generate(input).Should().Be(expected);
}
// ━━ Test fixtures — shared expensive setup (database, server) ━━
// IAsyncLifetime: async setup/teardown per test class
public class ProductServiceTests : IAsyncLifetime
{
private AppDbContext _db = null!;
public async Task InitializeAsync()
{
// Called once before any test in this class
_db = await TestDbContextFactory.CreateInMemoryAsync();
await _db.Products.AddRangeAsync(TestData.SampleProducts());
await _db.SaveChangesAsync();
}
public async Task DisposeAsync()
{
await _db.DisposeAsync();
}
[Fact]
public async Task GetByIdAsync_ExistingProduct_ReturnsProduct()
{
// Arrange
var existingId = TestData.SampleProducts().First().Id;
// Act
var product = await _db.Products.FindAsync(existingId);
// Assert
product.Should().NotBeNull();
product!.Id.Should().Be(existingId);
}
}
// Stub types for compilation
class SlugGenerator { public string Generate(string input) => input.ToLower().Replace(" ", "-").Replace("#", "").Replace("&", "").Trim(); }
class AppDbContext { public System.Collections.Generic.List<Product> Products { get; set; } = []; public Task<int> SaveChangesAsync() => Task.FromResult(0); public void Dispose() { } public ValueTask DisposeAsync() => ValueTask.CompletedTask; }
static class TestDbContextFactory { public static Task<AppDbContext> CreateInMemoryAsync() => Task.FromResult(new AppDbContext()); }
static class TestData { public static System.Collections.Generic.List<Product> SampleProducts() => [new Product { Id = Guid.NewGuid(), Name = "Test Product", Price = 9.99m }]; }
class Product { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } }Tip
Tip
Practice xUnit Test Structure Lifecycle in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
.NET provides a unified platform for building various application types
Practice Task
Note
Practice Task — (1) Write a working example of xUnit Test Structure Lifecycle from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with xUnit Test Structure Lifecycle is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready dotnet code.