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.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
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.
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.
using Xunit;
publicclassCalculatorTests
{
privatereadonly Calculator _calculator;
publicCalculatorTests()
{
// Constructor acts as setup - runs before each test
_calculator = new Calculator();
}
[Fact]
publicvoidAdd_TwoPositiveNumbers_ReturnsSum()
{
// Arrangevar a = 5;
var b = 3;
// Actvar result = _calculator.Add(a, b);
// Assert
Assert.Equal(8, result);
}
[Fact]
publicvoidDivide_ByZero_ThrowsDivideByZeroException()
{
// Act & Assertvar 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)]
publicvoidAdd_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
{
var result = _calculator.Add(a, b);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData(" ")]
publicvoidValidate_InvalidInput_ReturnsFalse(string input)
{
var result = _calculator.IsValidExpression(input);
Assert.False(result);
}
}
MemberData and ClassData for Complex Scenarios
publicclassUserServiceTests
{
privatereadonly Mock<IUserRepository> _mockRepo;
privatereadonly Mock<IEmailService> _mockEmail;
privatereadonly UserService _service;
publicUserServiceTests()
{
_mockRepo = new Mock<IUserRepository>();
_mockEmail = new Mock<IEmailService>();
_service = new UserService(_mockRepo.Object, _mockEmail.Object);
}
publicstatic IEnumerable<object[]> InvalidUserData =>
new List<object[]>
{
newobject[] { "", "valid@email.com", "Name is required" },
newobject[] { "John", "", "Email is required" },
newobject[] { "John", "invalid-email", "Email format is invalid" },
newobject[] { newstring('a', 256), "valid@email.com", "Name too long" },
};
[Theory]
[MemberData(nameof(InvalidUserData))]
publicasync Task CreateUser_InvalidData_ReturnsValidationError(string name, string email, string expectedError)
{
// Arrangevar request = new CreateUserRequest { Name = name, Email = email };
// Actvar result = await _service.CreateUser(request);
// Assert
Assert.False(result.IsSuccess);
Assert.Contains(expectedError, result.Error);
}
[Fact]
publicasync Task CreateUser_ValidData_SavesAndSendsWelcomeEmail()
{
// Arrangevar request = new CreateUserRequest { Name = "Alice", Email = "alice@test.com" };
_mockRepo.Setup(r => r.SaveAsync(It.IsAny<User>()))
.ReturnsAsync(new User { Id = 1, Name = "Alice", Email = "alice@test.com" });
_mockEmail.Setup(e => e.SendWelcomeEmail(It.IsAny<string>()))
.Returns(Task.CompletedTask);
// Actvar 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 == "alice@test.com")), Times.Once);
_mockEmail.Verify(e => e.SendWelcomeEmail("alice@test.com"), Times.Once);
}
}
Class Fixtures for Shared Context
// Fixture class - created once for all tests in the classpublicclassDatabaseFixture : IAsyncLifetime
{
publicstring ConnectionString { get; privateset; }
public AppDbContext DbContext { get; privateset; }
publicasync 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();
}
publicasync Task DisposeAsync()
{
await DbContext.Database.EnsureDeletedAsync();
await DbContext.DisposeAsync();
}
}
// Test class using the fixturepublicclassUserRepositoryTests : IClassFixture<DatabaseFixture>
{
privatereadonly DatabaseFixture _fixture;
privatereadonly UserRepository _repository;
publicUserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
_repository = new UserRepository(_fixture.DbContext);
}
[Fact]
publicasync Task GetById_ExistingUser_ReturnsUser()
{
// Arrangevar user = new User { Name = "Alice", Email = "alice@test.com" };
_fixture.DbContext.Users.Add(user);
await _fixture.DbContext.SaveChangesAsync();
// Actvar 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")]
publicclassDatabaseCollection : ICollectionFixture<DatabaseFixture>
{
// This class has no code, just the attributes
}
// First test class in the collection
[Collection("Database")]
publicclassUserRepositoryTests
{
privatereadonly DatabaseFixture _fixture;
publicUserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
publicasync Task CreateUser_ValidData_PersistsToDatabase()
{
var repo = new UserRepository(_fixture.DbContext);
var user = new User { Name = "Bob", Email = "bob@test.com" };
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")]
publicclassOrderRepositoryTests
{
privatereadonly DatabaseFixture _fixture;
publicOrderRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
publicasync 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
publicclassWebApplicationFixture : IAsyncLifetime
{
public HttpClient Client { get; privateset; }
private WebApplicationFactory<Program> _factory;
publicasync Task InitializeAsync()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace real database with in-memoryvar descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
// Seed test datavar sp = services.BuildServiceProvider();
usingvar scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
SeedTestData(db);
});
});
Client = _factory.CreateClient();
await Task.CompletedTask;
}
privatestaticvoidSeedTestData(AppDbContext db)
{
db.Users.Add(new User { Id = 1, Name = "TestUser", Email = "test@example.com" });
db.SaveChanges();
}
publicasync Task DisposeAsync()
{
Client?.Dispose();
await _factory.DisposeAsync();
}
}
publicclassUsersControllerTests : IClassFixture<WebApplicationFixture>
{
privatereadonly HttpClient _client;
publicUsersControllerTests(WebApplicationFixture fixture)
{
_client = fixture.Client;
}
[Fact]
publicasync Task GetUsers_ReturnsOkWithUserList()
{
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();
Assert.NotEmpty(users);
}
[Fact]
publicasync Task CreateUser_ValidPayload_ReturnsCreated()
{
var payload = new { Name = "NewUser", Email = "new@example.com" };
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]
publicasync Task CreateUser_DuplicateEmail_ReturnsConflict()
{
var payload = new { Name = "Duplicate", Email = "test@example.com" };
var response = await _client.PostAsJsonAsync("/api/users", payload);
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
}
}
Custom Assertions and Test Helpers
publicstaticclassAssertionExtensions
{
publicstaticvoidShouldBeValidEmail(string email)
{
Assert.Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$", email);
}
publicstaticvoidShouldBeWithinRange(decimalvalue, decimal min, decimal max)
{
Assert.InRange(value, min, max);
}
publicstaticasync 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 PatternpublicclassUserBuilder
{
privatestring _name = "Default User";
privatestring _email = "default@test.com";
privatestring _role = "User";
public UserBuilder WithName(string name) { _name = name; returnthis; }
public UserBuilder WithEmail(string email) { _email = email; returnthis; }
public UserBuilder WithRole(string role) { _role = role; returnthis; }
public UserBuilder AsAdmin() { _role = "Admin"; returnthis; }
public User Build() => new User { Name = _name, Email = _email, Role = _role };
}
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 static mutable 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>() or Assert.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 ITestOutputHelper for diagnostic logging instead of Console.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.