
dotnet-xunit
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
- Installs
- 613
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-xunit- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads dotnet-xunit automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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
}
// 2. Use in test classes
[Collection("Database")]
public class OrderRepositoryTests
{
private readonly DatabaseFixture _db;
public OrderRepositoryTests(DatabaseFixture db)
{
_db = db;
}
[Fact]
public async Task Insert_ValidOrder_Persists()
{
// Uses the shared database fixture
}
}
[Collection("Database")]
public class CustomerRepositoryTests
{
private readonly DatabaseFixture _db;
public CustomerRepositoryTests(DatabaseFixture db)
{
_db = db;
}
}
IAsyncLifetime on Test Classes
For per-test async setup/teardown without a shared fixture:
public class FileProcessorTests : IAsyncLifetime
{
private string _tempDir = "";
public ValueTask InitializeAsync()
{
_tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(_tempDir);
return ValueTask.CompletedTask;
}
public ValueTask DisposeAsync()
{
if (Directory.Exists(_tempDir))
Directory.Delete(_tempDir, recursive: true);
return ValueTask.CompletedTask;
}
[Fact]
public async Task Process_CsvFile_ExtractsRecords()
{
var filePath = Path.Combine(_tempDir, "data.csv");
await File.WriteAllTextAsync(filePath, "Name,Age\nAlice,30\nBob,25");
var processor = new FileProcessor();
var records = await processor.ProcessAsync(filePath);
Assert.Equal(2, records.Count);
}
}
Parallel Execution
Default Behavior
xUnit runs test classes within a collection sequentially but runs different collections in parallel. Each test class without an explicit [Collection] attribute is its own implicit collection, so by default test classes run in parallel.
Controlling Parallelism
Disable Parallelism for Specific Tests
Place tests that share mutable state in the same collection:
[CollectionDefinition("Sequential", DisableParallelization = true)]
public class SequentialCollection { }
[Collection("Sequential")]
public class StatefulServiceTests
{
// These tests run sequentially within this collection
}
Assembly-Level Configuration
Create xunit.runner.json in the test project root:
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeAssembly": false,
"parallelizeTestCollections": true,
"maxParallelThreads": 4
}
Ensure it is copied to output:
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
v2 compatibility note: In v2, configuration was via xunit.runner.json or assembly attributes. v3 retains xunit.runner.json support with the same property names.
Test Output
ITestOutputHelper
Capture diagnostic output that appears in test results:
public class DiagnosticTests
{
private readonly ITestOutputHelper _output;
public DiagnosticTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task ProcessBatch_LargeDataset_CompletesWithinTimeout()
{
var sw = Stopwatch.StartNew();
var result = await processor.ProcessBatchAsync(largeDataset);
sw.Stop();
_output.WriteLine($"Processed {result.Count} items in {sw.ElapsedMilliseconds}ms");
Assert.True(sw.Elapsed < TimeSpan.FromSeconds(5));
}
}
Integrating with ILogger
Bridge xUnit output to Microsoft.Extensions.Logging for integration tests:
// NuGet: Microsoft.Extensions.Logging (for LoggerFactory)
// + a logging provider that writes to ITestOutputHelper
// Common approach: use a simple adapter
public class XunitLoggerProvider : ILoggerProvider
{
private readonly ITestOutputHelper _output;
public XunitLoggerProvider(ITestOutputHelper output) => _output = output;
public ILogger CreateLogger(string categoryName) =>
new XunitLogger(_output, categoryName);
public void Dispose() { }
}
public class XunitLogger(ITestOutputHelper output, string category) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception? exception, Func<TState, Exception?, string> formatter)
{
output.WriteLine($"[{logLevel}] {category}: {formatter(state, exception)}");
if (exception is not null)
output.WriteLine(exception.ToString());
}
}
Custom Assertions
Extending Assert with Custom Methods
Create domain-specific assertions for cleaner test code:
public static class OrderAssert
{
public static void HasStatus(Order order, OrderStatus expected)
{
Assert.NotNull(order);
if (order.Status != expected)
{
throw Xunit.Sdk.EqualException.ForMismatchedValues(
expected, order.Status);
}
}
public static void ContainsItem(Order order, string sku, int quantity)
{
Assert.NotNull(order);
var item = Assert.Single(order.Items, i => i.Sku == sku);
Assert.Equal(quantity, item.Quantity);
}
}
// Usage
[Fact]
public void Complete_ValidOrder_SetsCompletedStatus()
{
var order = new Order();
order.Complete();
OrderAssert.HasStatus(order, OrderStatus.Completed);
}
Using Assert.Multiple (xUnit v3)
Group related assertions so all are evaluated even if one fails:
[Fact]
public void CreateOrder_ValidRequest_SetsAllProperties()
{
var order = OrderFactory.Create(request);
Assert.Multiple(
() => Assert.Equal("cust-123", order.CustomerId),
() => Assert.Equal(OrderStatus.Pending, order.Status),
() => Assert.NotEqual(Guid.Empty, order.Id),
() => Assert.NotEmpty(order.Items)
);
}
v2 compatibility note: Assert.Multiple is new in xUnit v3. In v2, use separate assertions -- the test stops at the first failure.
xUnit Analyzers
The xunit.analyzers package (included with xUnit v3) catches common test authoring mistakes at compile time.
Important Rules
| Rule | Description | Severity |
|---|---|---|
xUnit1004 | Test methods should not be skipped | Info |
xUnit1012 | Null should not be used for value type parameters | Warning |
xUnit1025 | InlineData should be unique within a Theory | Warning |
xUnit2000 | Constants and literals should be the expected argument | Warning |
xUnit2002 | Do not use null check on value type | Warning |
xUnit2007 | Do not use typeof expression to check type | Warning |
xUnit2013 | Do not use equality check to check collection size | Warning |
xUnit2017 | Do not use Contains() to check if value exists in a set | Warning |
Suppressing Specific Rules
In .editorconfig for test projects:
[tests/**.cs]
# Allow skipped tests during development
dotnet_diagnostic.xUnit1004.severity = suggestion
Key Principles
- One fact per
[Fact], one concept per[Theory]. If a[Theory]tests fundamentally different scenarios, split into separate[Fact]methods. - Use
IClassFixturefor expensive shared resources within a single test class. UseICollectionFixturewhen multiple classes share the same resource. - Do not disable parallelism globally. Instead, group tests that share mutable state into named collections.
- Use
IAsyncLifetimefor async setup/teardown instead of constructors andIDisposable. Constructors cannot be async, andIDisposable.Dispose()does not await. - Keep test data close to the test. Prefer
[InlineData]for simple cases. Use[MemberData]or[ClassData]only when data is complex or shared. - Enable xUnit analyzers in all test projects. They catch common mistakes that lead to false-passing or flaky tests.
Agent Gotchas
- Do not use constructor-injected
ITestOutputHelperin static methods.ITestOutputHelperis per-test-instance; store it in an instance field, not a static one. - Do not forget to make fixture classes
public. xUnit requires fixture types to be public with a public parameterless constructor (orIAsyncLifetime). Non-public fixtures cause silent failures. - Do not mix
[Fact]and[Theory]on the same method. A method is either a fact or a theory, not both. - Do not return
voidfrom async test methods. ReturnTaskorValueTask.async voidtests report false success because xUnit cannot observe the async completion. - Do not use
[Collection]without a matching[CollectionDefinition]. An unmatched collection name silently creates an implicit collection with default behavior, defeating the purpose.
References
FAQ
What does the dotnet-xunit skill do?
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
How do I install dotnet-xunit?
Run npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-xunit in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/dotnet-xunit/ (personal) or .claude/skills/dotnet-xunit/ (project).
Which AI agents can use dotnet-xunit?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is dotnet-xunit safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains dotnet-xunit?
It is published by wshaddix in wshaddix/dotnet-skills. It has 613 recorded installs.
More from wshaddix/dotnet-skills
dotnet-testing-strategy
wshaddix/dotnet-skills
Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.
modern-csharp-coding-standards
wshaddix/dotnet-skills
Write modern, high-performance C code using records, pattern matching, value objects, async/await, Span /Memory , and best-practice API design patterns. Emphasizes functional-style programming with C 12+ features. Use when writing new C code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
csharp-wolverinefx
wshaddix/dotnet-skills
Build .NET applications with WolverineFX for messaging, HTTP services, and event sourcing. Use when implementing command handlers, message handlers, HTTP endpoints with WolverineFx.HTTP, transactional outbox patterns, event sourcing with Marten, CQRS architectures, cascading messages, batch message processing, or configuring transports like RabbitMQ, Azure Service Bus, or Amazon SQS.
testcontainers
wshaddix/dotnet-skills
Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.
microsoft-extensions-dependency-injection
wshaddix/dotnet-skills
Dependency injection patterns and best practices using Microsoft.Extensions.DependencyInjection for .NET applications. Use when configuring DI containers in .NET, choosing between service lifetimes (Singleton, Scoped, Transient), or implementing decorator patterns and service interception.
logging-observability
wshaddix/dotnet-skills
Production-grade logging and observability patterns for ASP.NET Core Razor Pages. Covers structured logging with Serilog, correlation IDs, health checks, request logging, OpenTelemetry integration, and diagnostic best practices. Use when setting up structured logging in ASP.NET Core applications, implementing distributed tracing with OpenTelemetry, or configuring health checks and observability.
Popular backend & apis skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
entra-app-registration
microsoft/azure-skills
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.
google-agents-cli-adk-code
google/agents-cli
This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).
embedded-captions
heygen-com/hyperframes
Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.
entra-agent-id
microsoft/azure-skills
Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).
neon
neondatabase/agent-skills
Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.