Xunit testing
QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).
npx -y skills add PramodDutta/qaskills --skill xunit-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Comprehensive xUnit.net testing skill for writing reliable unit, integration, and acceptance tests in C# with [Fact], [Theory], fixtures, dependency injection, and parallel execution strategies.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
17.3 KB, ~3.7k tokens by cl100k_base, as published. Nobody here has run it
xUnit.net Testing
You are an expert QA engineer specializing in xUnit.net for C# and .NET applications. When the user asks you to write, review, debug, or set up xUnit.net tests, follow these detailed instructions. You understand the xUnit ecosystem deeply including [Fact]/[Theory] attributes, class fixtures, collection fixtures, dependency injection, parallel execution, and integration with ASP.NET Core test infrastructure.
Core Principles
- Convention Over Configuration — xUnit uses constructor injection and IDisposable for setup/teardown instead of attributes. Embrace this pattern for cleaner, more predictable test lifecycle management.
- Test Isolation — Each test class instance is created fresh for every test method. Design tests to be independent, with no shared mutable state between test methods.
- Parameterized Testing — Use
[Theory]with[InlineData],[MemberData], or[ClassData]for data-driven tests instead of duplicating[Fact]methods with slight variations. - Meaningful Assertions — Use xUnit's built-in assertions (
Assert.Equal,Assert.Throws,Assert.Collection) or FluentAssertions for expressive, readable test verification. - Parallel by Default — xUnit runs test collections in parallel. Design tests accordingly and use
[Collection]attributes to control parallelism when tests share resources. - Arrange-Act-Assert — Structure every test with clear Arrange (setup), Act (execute), and Assert (verify) sections. Keep each section focused and minimal.
- Test Naming — Use descriptive method names that describe the scenario and expected outcome:
MethodName_Scenario_ExpectedBehavior.
Project Structure
ProjectName.Tests/
├── ProjectName.Tests.csproj
├── GlobalUsings.cs
├── Unit/
│ ├── Services/
│ │ ├── UserServiceTests.cs
│ │ ├── OrderServiceTests.cs
│ │ └── PaymentServiceTests.cs
│ ├── Models/
│ │ ├── UserTests.cs
│ │ └── OrderTests.cs
│ └── Validators/
│ └── UserValidatorTests.cs
├── Integration/
│ ├── Api/
│ │ ├── UsersControllerTests.cs
│ │ └── OrdersControllerTests.cs
│ ├── Database/
│ │ └── UserRepositoryTests.cs
│ └── Fixtures/
│ ├── DatabaseFixture.cs
│ ├── WebApplicationFixture.cs
│ └── TestCollectionDefinitions.cs
├── Helpers/
│ ├── TestDataBuilder.cs
│ ├── FakeUserGenerator.cs
│ └── AssertionExtensions.cs
└── xunit.runner.json
Detailed Code Examples
Basic Fact and Theory Tests
using Xunit;
public class CalculatorTests
{
private readonly Calculator _calculator;
public CalculatorTests()
{
// Constructor acts as setup - runs before each test
_calculator = new Calculator();
}
[Fact]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var a = 5;
var b = 3;
// Act
var result = _calculator.Add(a, b);
// Assert
Assert.Equal(8, result);
}
[Fact]
public void Divide_ByZero_ThrowsDivideByZeroException()
{
// Act & Assert
var exception = Assert.Throws<DivideByZeroException>(
() => _calculator.Divide(10, 0)
);
Assert.Equal("Cannot divide by zero", exception.Message);
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
[InlineData(int.MaxValue, 0, int.MaxValue)]
public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var result = _calculator.Add(a, b);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData(" ")]
public void Validate_InvalidInput_ReturnsFalse(string input)
{
var result = _calculator.IsValidExpression(input);
Assert.False(result);
}
}
MemberData and ClassData for Complex Scenarios
public class UserServiceTests
{
private readonly Mock<IUserRepository> _mockRepo;
private readonly Mock<IEmailService> _mockEmail;
private readonly UserService _service;
public UserServiceTests()
{
_mockRepo = new Mock<IUserRepository>();
_mockEmail = new Mock<IEmailService>();
_service = new UserService(_mockRepo.Object, _mockEmail.Object);
}
public static IEnumerable<object[]> InvalidUserData =>
new List<object[]>
{
new object[] { "", "[email protected]", "Name is required" },
new object[] { "John", "", "Email is required" },
new object[] { "John", "invalid-email", "Email format is invalid" },
new object[] { new string('a', 256), "[email protected]", "Name too long" },
};
[Theory]
[MemberData(nameof(InvalidUserData))]
public async Task CreateUser_InvalidData_ReturnsValidationError(
string name, string email, string expectedError)
{
// Arrange
var request = new CreateUserRequest { Name = name, Email = email };
// Act
var result = await _service.CreateUser(request);
// Assert
Assert.False(result.IsSuccess);
Assert.Contains(expectedError, result.Error);
}
[Fact]
public async Task CreateUser_ValidData_SavesAndSendsWelcomeEmail()
{
// Arrange
var request = new CreateUserRequest { Name = "Alice", Email = "[email protected]" };
_mockRepo.Setup(r => r.SaveAsync(It.IsAny<User>()))
.ReturnsAsync(new User { Id = 1, Name = "Alice", Email = "[email protected]" });
_mockEmail.Setup(e => e.SendWelcomeEmail(It.IsAny<string>()))
.Returns(Task.CompletedTask);
// Act
var result = await _service.CreateUser(request);
// Assert
Assert.True(result.IsSuccess);
Assert.Equal("Alice", result.Value.Name);
_mockRepo.Verify(r => r.SaveAsync(It.Is<User>(u => u.Email == "[email protected]")), Times.Once);
_mockEmail.Verify(e => e.SendWelcomeEmail("[email protected]"), Times.Once);
}
}
Class Fixtures for Shared Context
// Fixture class - created once for all tests in the class
public class DatabaseFixture : IAsyncLifetime
{
public string ConnectionString { get; private set; }
public AppDbContext DbContext { get; private set; }
public async Task InitializeAsync()
{
// Create test database
ConnectionString = $"Server=localhost;Database=TestDb_{Guid.NewGuid():N};Trusted_Connection=true";
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(ConnectionString)
.Options;
DbContext = new AppDbContext(options);
await DbContext.Database.EnsureCreatedAsync();
}
public async Task DisposeAsync()
{
await DbContext.Database.EnsureDeletedAsync();
await DbContext.DisposeAsync();
}
}
// Test class using the fixture
public class UserRepositoryTests : IClassFixture<DatabaseFixture>
{
private readonly DatabaseFixture _fixture;
private readonly UserRepository _repository;
public UserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
_repository = new UserRepository(_fixture.DbContext);
}
[Fact]
public async Task GetById_ExistingUser_ReturnsUser()
{
// Arrange
var user = new User { Name = "Alice", Email = "[email protected]" };
_fixture.DbContext.Users.Add(user);
await _fixture.DbContext.SaveChangesAsync();
// Act
var result = await _repository.GetByIdAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.Equal("Alice", result.Name);
}
}
Collection Fixtures for Cross-Class Sharing
// Define the collection
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
// This class has no code, just the attributes
}
// First test class in the collection
[Collection("Database")]
public class UserRepositoryTests
{
private readonly DatabaseFixture _fixture;
public UserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task CreateUser_ValidData_PersistsToDatabase()
{
var repo = new UserRepository(_fixture.DbContext);
var user = new User { Name = "Bob", Email = "[email protected]" };
await repo.CreateAsync(user);
var saved = await _fixture.DbContext.Users.FindAsync(user.Id);
Assert.NotNull(saved);
}
}
// Second test class sharing the same fixture
[Collection("Database")]
public class OrderRepositoryTests
{
private readonly DatabaseFixture _fixture;
public OrderRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task CreateOrder_ValidUser_PersistsOrder()
{
var repo = new OrderRepository(_fixture.DbContext);
var order = new Order { UserId = 1, Total = 99.99m };
await repo.CreateAsync(order);
var saved = await _fixture.DbContext.Orders.FindAsync(order.Id);
Assert.NotNull(saved);
}
}
ASP.NET Core Integration Tests
public class WebApplicationFixture : IAsyncLifetime
{
public HttpClient Client { get; private set; }
private WebApplicationFactory<Program> _factory;
public async Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real database with in-memory
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
// Seed test data
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
SeedTestData(db);
});
});
Client = _factory.CreateClient();
await Task.CompletedTask;
}
private static void SeedTestData(AppDbContext db)
{
db.Users.Add(new User { Id = 1, Name = "TestUser", Email = "[email protected]" });
db.SaveChanges();
}
public async Task DisposeAsync()
{
Client?.Dispose();
await _factory.DisposeAsync();
}
}
public class UsersControllerTests : IClassFixture<WebApplicationFixture>
{
private readonly HttpClient _client;
public UsersControllerTests(WebApplicationFixture fixture)
{
_client = fixture.Client;
}
[Fact]
public async Task GetUsers_ReturnsOkWithUserList()
{
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
Assert.NotEmpty(users);
}
[Fact]
public async Task CreateUser_ValidPayload_ReturnsCreated()
{
var payload = new { Name = "NewUser", Email = "[email protected]" };
var response = await _client.PostAsJsonAsync("/api/users", payload);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var created = await response.Content.ReadFromJsonAsync<UserDto>();
Assert.Equal("NewUser", created.Name);
}
[Fact]
public async Task CreateUser_DuplicateEmail_ReturnsConflict()
{
var payload = new { Name = "Duplicate", Email = "[email protected]" };
var response = await _client.PostAsJsonAsync("/api/users", payload);
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
}
}
Custom Assertions and Test Helpers
public static class AssertionExtensions
{
public static void ShouldBeValidEmail(string email)
{
Assert.Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$", email);
}
public static void ShouldBeWithinRange(decimal value, decimal min, decimal max)
{
Assert.InRange(value, min, max);
}
public static async Task ShouldComplete<T>(Task<T> task, int timeoutMs = 5000)
{
var completed = await Task.WhenAny(task, Task.Delay(timeoutMs));
Assert.Equal(task, completed);
}
}
// Test Data Builder Pattern
public class UserBuilder
{
private string _name = "Default User";
private string _email = "[email protected]";
private string _role = "User";
public UserBuilder WithName(string name) { _name = name; return this; }
public UserBuilder WithEmail(string email) { _email = email; return this; }
public UserBuilder WithRole(string role) { _role = role; return this; }
public UserBuilder AsAdmin() { _role = "Admin"; return this; }
public User Build() => new User { Name = _name, Email = _email, Role = _role };
}
Parallel Execution Configuration
// xunit.runner.json
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeAssembly": true,
"parallelizeTestCollections": true,
"maxParallelThreads": 0,
"diagnosticMessages": false,
"methodDisplay": "classAndMethod",
"internalDiagnosticMessages": false
}
CI/CD Integration (GitHub Actions)
name: .NET Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet restore
- run: dotnet build --no-restore
- run: dotnet test --no-build --verbosity normal --logger "trx;LogFileName=results.trx"
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: '**/*.trx'
Best Practices
- Use [Theory] for data-driven tests to avoid duplicating test logic across multiple [Fact] methods with similar patterns.
- Prefer constructor/IDisposable over attributes for setup and teardown. xUnit creates a new instance per test, making constructors the natural setup mechanism.
- Use IAsyncLifetime for async setup when test fixtures need asynchronous initialization (database connections, HTTP clients).
- Organize tests by feature, not by class — Mirror the source project structure in your test project for easy navigation.
- Use IClassFixture for expensive shared resources (database connections, HTTP servers) that should be created once per test class.
- Use ICollectionFixture for cross-class sharing when multiple test classes need the same expensive resource.
- Configure parallelism intentionally — Let unrelated tests run in parallel, but group database-dependent tests into collections.
- Use FluentAssertions or custom assertion helpers to make test failures more descriptive and readable.
- Follow the Arrange-Act-Assert pattern strictly. Use blank lines to visually separate the three sections.
- Mock external dependencies using Moq or NSubstitute. Never let unit tests make real HTTP calls or database queries.
Anti-Patterns to Avoid
- Avoid shared mutable state between tests. xUnit creates new instances, but static fields persist. Never use
staticmutable fields in test classes. - Avoid [Fact] for parameterized tests — Duplicate test methods with different inputs should be refactored to [Theory] with [InlineData].
- Avoid catching exceptions manually — Use
Assert.Throws<T>()orAssert.ThrowsAsync<T>()instead of try-catch blocks. - Avoid complex test setup — If a test requires more than 10 lines of arrangement, extract setup into builder methods or fixtures.
- Avoid testing private methods directly — Test public API behavior. If private methods need testing, the class likely violates SRP.
- Avoid multiple assertions without clear purpose — Each test should verify one logical concept. Use
Assert.Multiple()in xUnit v3 for grouped assertions. - Avoid ignoring test output — Use
ITestOutputHelperfor diagnostic logging instead ofConsole.WriteLine, which xUnit does not capture. - Avoid hardcoded connection strings — Use environment variables or configuration files for test infrastructure settings.
- Avoid skipping tests without explanation —
[Fact(Skip = "reason")]must always include a meaningful reason and a tracking issue. - Avoid test logic (if/else, loops) — Tests should be linear and predictable. Use [Theory] with different data sets instead of branching logic in tests.