Dotnet Xunit
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
- Skill ID
- wshaddix/dotnet-skills/dotnet-xunit
- Publisher
- wshaddix
- Repository
- dotnet-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
wshaddix/dotnet-skills/dotnet-xunitInstalls these files- SKILL.md
What this skill tells the agent
dotnet-xunit
xUnit v3 testing framework features for .NET. Covers [Fact] and [Theory] attributes, test fixtures (IClassFixture, ICollectionFixture), parallel execution configuration, IAsyncLifetime for async setup/teardown, custom assertions, and xUnit analyzers. Includes v2 compatibility notes where behavior differs.
Version assumptions: xUnit v3 primary (.NET 8.0+ baseline). Where v3 behavior differs from v2, compatibility notes are provided inline. xUnit v2 remains widely used; many projects will encounter both versions during migration.
Out of scope: Test project scaffolding (creating xUnit projects, package references, Directory.Build.props) is owned by [skill:dotnet-add-testing]. Testing strategy and test type decisions are covered by [skill:dotnet-testing-strategy]. Integration testing patterns (WebApplicationFactory, Testcontainers) are covered by [skill:dotnet-integration-testing].
Prerequisites: Test project already scaffolded via [skill:dotnet-add-testing] with xUnit packages referenced. Run [skill:dotnet-version-detection] to confirm .NET 8.0+ baseline for xUnit v3 support.
Cross-references: [skill:dotnet-testing-strategy] for deciding what to test and how, [skill:dotnet-integration-testing] for combining xUnit with WebApplicationFactory and Testcontainers.
xUnit v3 vs v2: Key Changes
| Feature | xUnit v2 | xUnit v3 |
|---|---|---|
| Package | xunit (2.x) | xunit.v3 |
| Runner | xunit.runner.visualstudio | xunit.runner.visualstudio (3.x) |
| Async lifecycle | IAsyncLifetime | IAsyncLifetime (now returns ValueTask) |
| Assert package | Bundled | Separate xunit.v3.assert (or xunit.v3.assert.source for extensibility) |
| Parallelism default | Per-collection | Per-collection (same, but configurable per-assembly) |
| Timeout | Timeout property on [Fact] and [Theory] | Timeout property on [Fact] and [Theory] (unchanged) |
| Test output | ITestOutputHelper | ITestOutputHelper (unchanged) |
| `[ClassData]` | Returns IEnumerable<object[]> | Returns IEnumerable<TheoryDataRow<T>> (strongly typed) |
| `[MemberData]` | Returns IEnumerable<object[]> | Supports TheoryData<T> and TheoryDataRow<T> |
| Assertion messages | Optional string parameter on Assert methods | Removed in favor of custom assertions (v3.0); use Assert.Fail() for explicit messages |
v2 compatibility note: If migrating from v2, replace xunit package with xunit.v3. Most [Fact] and [Theory] tests work without changes. The primary migration effort is in IAsyncLifetime (return type changes to ValueTask), [ClassData] (strongly typed row format), and removed assertion message parameters.
Facts and Theories
[Fact] -- Single Test Case
Use [Fact] for tests with no parameters:
public class DiscountCalculatorTests
{
[Fact]
public void Apply_NegativePercentage_ThrowsArgumentOutOfRangeException()
{
var calculator = new DiscountCalculator();
var ex = Assert.Throws<ArgumentOutOfRangeException>(
() => calculator.Apply(100m, percentage: -5));
Assert.Equal("percentage", ex.ParamName);
}
[Fact]
public async Task ApplyAsync_ValidDiscount_ReturnsDiscountedPrice()
{
var calculator = new DiscountCalculator();
var result = await calculator.ApplyAsync(100m, percentage: 15);
Assert.Equal(85m, result);
}
}[Theory] -- Parameterized Tests
Use [Theory] to run the same test logic with different inputs.
[InlineData]
Best for simple value types:
[Theory]
[InlineData(100, 10, 90)] // 10% off 100 = 90
[InlineData(200, 25, 150)] // 25% off 200 = 150
[InlineData(50, 0, 50)] // 0% off = no change
[InlineData(100, 100, 0)] // 100% off = 0
public void Apply_VariousInputs_ReturnsExpectedPrice(
decimal price, decimal percentage, decimal expected)
{
var calculator = new DiscountCalculator();
var result = calculator.Apply(price, percentage);
Assert.Equal(expected, result);
}[MemberData] with TheoryData<T>
Best for complex data or shared datasets:
public class OrderValidatorTests
{
public static TheoryData<Order, bool> ValidationCases => new()
{
{ new Order { Items = [new("SKU-1", 1)], CustomerId = "C1" }, true },
{ new Order { Items = [], CustomerId = "C1" }, false }, // no items
{ new Order { Items = [new("SKU-1", 1)], CustomerId = "" }, false }, // no customer
};
[Theory]
[MemberData(nameof(ValidationCases))]
public void IsValid_VariousOrders_ReturnsExpected(Order order, bool expected)
{
var validator = new OrderValidator();
var result = validator.IsValid(order);
Assert.Equal(expected, result);
}
}[ClassData]
Best for data shared across multiple test classes:
// xUnit v3: use TheoryDataRow<T> for strongly-typed rows
public class CurrencyConversionData : IEnumerable<TheoryDataRow<string, string, decimal>>
{
public IEnumerator<TheoryDataRow<string, string, decimal>> GetEnumerator()
{
yield return new("USD", "EUR", 0.92m);
yield return new("GBP", "USD", 1.27m);
yield return new("EUR", "GBP", 0.86m);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// xUnit v2 compatibility: v2 uses IEnumerable<object[]> instead of TheoryDataRow<T>
// public class CurrencyConversionData : IEnumerable<object[]>
// {
// public IEnumerator<object[]> GetEnumerator()
// {
// yield return new object[] { "USD", "EUR", 0.92m };
// }
// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// }
[Theory]
[ClassData(typeof(CurrencyConversionData))]
public void Convert_KnownPairs_ReturnsExpectedRate(
string from, string to, decimal expectedRate)
{
var converter = new CurrencyConverter();
var rate = converter.GetRate(from, to);
Assert.Equal(expectedRate, rate, precision: 2);
}Fixtures: Shared Setup and Teardown
Fixtures provide shared, expensive resources across tests while maintaining test isolation.
IClassFixture<T> -- Shared Per Test Class
Use when multiple tests in the same class share an expensive resource (database connection, configuration):
public class DatabaseFixture : IAsyncLifetime
{
public string ConnectionString { get; private set; } = "";
public ValueTask InitializeAsync()
{
// xUnit v3: returns ValueTask (v2 returns Task)
ConnectionString = $"Host=localhost;Database=test_{Guid.NewGuid():N}";
// Create database, run migrations, etc.
return ValueTask.CompletedTask;
}
public ValueTask DisposeAsync()
{
// xUnit v3: returns ValueTask (v2 returns Task)
// Drop database
return ValueTask.CompletedTask;
}
}
public class OrderRepositoryTests : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _db;
public OrderRepositoryTests(DatabaseFixture db)
{
_db = db;
// Each test gets the shared database fixture
}
[Fact]
public async Task GetById_ExistingOrder_ReturnsOrder()
{
var repo = new OrderRepository(_db.ConnectionString);
var result = await repo.GetByIdAsync(KnownOrderId);
Assert.NotNull(result);
}
}v2 compatibility note: In xUnit v2, IAsyncLifetime.InitializeAsync() and DisposeAsync() return Task. In v3, they return ValueTask. When migrating, change the return types accordingly.
ICollectionFixture<T> -- Shared Across Test Classes
Use when multiple test classes need the same expensive resource:
// 1. Define the collection
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
// This class has no code -- it is a marker for the collection
}
More from wshaddix
Use this skill in a project
Open any RiverX project, search the Skills panel for wshaddix/dotnet-skills/dotnet-xunit, and install it in one click.
