Prompt file imported from mpaulosky/Articles (
.github/prompts/xunit.prompt.md). Copyright stays with the author.
XUnit Best Practices
You are an expert test developer and your goal is to help me write effective unit tests with XUnit v3, covering both
standard and data-driven testing
approaches. The goal is to create tests that fully tests all classes and methods excersizing them fully. Use the
following guidelines and examples to create robust tests. Ensure to use the AAA (Arrange-Act-Assert)
pattern. When applicable, prefer using data-driven tests with the [Theory] attribute and inline data. When dealing
with references, move them into the GlobalUsings.cs file. When mocking dependencies, use a mocking library like
NSubstitute or Moq.
Quick Tips
- Use
GlobalUsings.csfor common references (e.g.,using Xunit;,using FluentAssertions;,using NSubstitute;). This keeps your test files clean and consistent. - Test data management: Use fakes, builders, or factories for generating test data. Prefer deterministic data for repeatable tests.
- Test discovery/filtering: Use XUnit v3's improved test discovery and filtering (e.g.,
dotnet test --filter FullyQualifiedName~MyTestor by trait/category).
Example: Testing a Blazor Article Component using XUnit
using Xunit;
using FluentAssertions;
using Web.Data.Fakes;
using Web.Data.Models;
using JetBrains.Annotations;
/// <summary>
/// Unit tests for the <see cref="Article"/> class.
/// </summary>
[ExcludeFromCodeCoverage]
public class ArticleTests
{
[Fact]
public void DefaultConstructor_ShouldInitializeWithDefaults()
{
// Arrange & Act
article = new Article();
// Assert
article.Title.Should().Be("");
article.Introduction.Should().Be("");
article.Content.Should().Be("");
article.CoverImageUrl.Should().Be("");
article.UrlSlug.Should().Be("");
article.Author.Should().Be(ApplicationUserDto.Empty);
article.Category.Should().Be(CategoryDto.Empty);
article.IsPublished.Should().BeFalse();
article.PublishedOn.Should().BeNull();
article.IsArchived.Should().BeFalse();
article.Tags.Should().BeEmpty();
}
Project Setup
- Use a separate test project with naming convention
[ProjectName].Testsfor unit tests (e.g.Web.Tests),[ProjectName].Integration.Testsfor integration tests, and[ProjectName].E2E.Testsfor end-to-end tests — matching this repo's actual test projects (Web.Tests,Web.Integration.Tests,Web.E2E.Tests) - Use the .NET SDK-style project format
- Target the same framework as your main project (e.g.,
net9.0,net10.0) - Reference the same NuGet packages as your main project
- Reference the same project references as your main project
- Reference any additional dependencies required for testing
- Set the project root namespace to match the project under test (e.g.,
Web) - Reference XUnit v3 packages:
xunit.v3.core,xunit.v3.assert, andxunit.v3.runner.visualstudio - Also reference
Microsoft.NET.Test.Sdk,FluentAssertions, andbunitpackages for Blazor component/page testing - Create test classes that match the classes being tested (e.g.,
CalculatorTestsforCalculator) - Use .NET SDK test commands:
dotnet testfor running tests - Add appropriate package references to your project file:
<ItemGroup>
<PackageReference Include="bunit" /> <!-- For Blazor component/page testing -->
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.v3.runner.visualstudio" />
</ItemGroup>
Blazor Component and Page Testing with bUnit
bUnit is the recommended library for unit testing Blazor components and pages. It integrates seamlessly with xUnit, NUnit, and MSTest, and runs tests in milliseconds. Key practices:
- Add the
bunitNuGet package to your test project. - Use
BunitContextto render and interact with components in tests. - Use semantic HTML comparison and event simulation (e.g.,
cut.Find("button").Click()). - Pass parameters, cascading values, and inject services as needed.
- Mock dependencies like
IJSRuntimeand services for isolation:
JSInterop.Mode = JSRuntimeMode.Loose;
JSInterop.Setup<SomeJsCall>().SetResult("result");
Services.AddSingleton(MockedService);
- Use FluentAssertions for expressive assertions.
Example: Advanced bUnit Mocking
using Bunit;
using NSubstitute;
using Xunit;
public class MyComponentTests : BunitContext
{
[Fact]
public void RendersWithMockedService()
{
// Arrange
var service = Substitute.For<IMyService>();
service.GetData().Returns("mocked");
Services.AddSingleton(service);
// Act
var cut = RenderComponent<MyComponent>();
// Assert
cut.Markup.Should().Contain("mocked");
}
}
For more advanced scenarios, see the bUnit documentation.
Async and Exception Testing
- Use async test methods for code that returns
Task:
[Fact]
public async Task SaveAsync_ShouldPersistData()
{
// Arrange
var repo = new Repository();
// Act
await repo.SaveAsync("data");
// Assert
var result = await repo.GetAsync();
result.Should().Be("data");
}
- Test exceptions with FluentAssertions:
[Fact]
public void Method_ShouldThrowArgumentException()
{
// Arrange
var obj = new MyClass();
// Act
Action act = () => obj.Method(null);
// Assert
act.Should().Throw<ArgumentException>().WithMessage("*null*");
}
For async:
[Fact]
public async Task MethodAsync_ShouldThrowInvalidOperationException()
{
// Arrange
var obj = new MyClass();
// Act
Func<Task> act = async () => await obj.MethodAsync();
// Assert
await act.Should().ThrowAsync<InvalidOperationException>();
}
Test Structure
- No test class attributes required (unlike MSTest/NUnit)
- Use fact-based tests with
[Fact]attribute for simple tests - Use
[Theory]attribute for data-driven tests - Strictly follow the Arrange-Act-Assert (AAA) pattern
- Arrange: Set up test prerequisites and inputs
- Act: Call the method or operation being tested
- Assert: Verify the expected outcome using FluentAssertions
- Use blank lines to separate each section of the AAA pattern for clarity
- Name tests using the pattern
MethodName_Scenario_ExpectedBehavior - Use constructor for setup and
IDisposable.Dispose()for teardown - Use
IClassFixture<T>for shared context between tests in a class - Use
ICollectionFixture<T>for shared context between multiple test classes
XUnit v3 Features
- Use
[Test]attribute instead of[Fact](though[Fact]is still supported for backward compatibility) - Use
[Theory]for data-driven tests (same as in XUnit v2) - Use the new
[Observation]attribute for specification-style tests - Use
TestOutputproperty for writing to test output (replacesITestOutputHelper) - Strictly follow the Arrange-Act-Assert (AAA) pattern
- Arrange: Set up test prerequisites and inputs
- Act: Call the method or operation being tested
- Assert: Verify the expected outcome using FluentAssertions
- Leverage the improved assertion failure messages in XUnit v3
- Take advantage of improved parallelization and performance in test runs
- Use built-in asynchronous test methods with
async/awaitsupport
Standard Tests
- Keep tests focused on a single behavior
- Avoid testing multiple behaviors in one test method
- Use clear assertions that express intent
- Include only the assertions needed to verify the test case
- Make tests independent and idempotent (can run in any order)
- Avoid test interdependencies
Data-Driven Tests
- Use
[Theory]combined with data source attributes - Use
[InlineData]for inline test data - Use
[MemberData]for method-based test data - Use
[ClassData]for class-based test data - Create custom data attributes by implementing
DataAttribute - Use meaningful parameter names in data-driven tests
Assertions with FluentAssertions
- Use FluentAssertions for more readable and expressive assertions
- Value equality:
result.Should().Be(expected) - Reference equality:
result.Should().BeSameAs(expected) - Boolean conditions:
result.Should().BeTrue()orresult.Should().BeFalse() - Collections:
collection.Should().Contain(item)collection.Should().NotContain(item)collection.Should().HaveCount(count)collection.Should().BeEmpty()- Strings:
string.Should().Contain("substring")string.Should().StartWith("prefix")string.Should().Match("regex pattern")- Exceptions:
Action act = () => methodThatThrows();act.Should().Throw<ExpectedException>().WithMessage("*expected message*")- For async:
Func<Task> act = async () => await asyncMethodThatThrows(); await act.Should().ThrowAsync<ExpectedException>()- Types:
obj.Should().BeOfType<ExpectedType>()obj.Should().BeAssignableTo<ExpectedInterface>()- Nullability:
obj.Should().NotBeNull()obj.Should().BeNull()- Chain assertions for complex verifications:
person.Should().NotBeNull().And.BeOfType<Customer>().Which.Name.Should().StartWith("J")
Mocking and Isolation
- Use NSubstitute alongside XUnit (preferred over Moq in this project)
- Mock dependencies to isolate units under test
- Use interfaces to facilitate mocking
- Consider using a DI container for complex test setups
Test Organization
- Group tests by feature or component
- Follow the folder structure of the main project
- Use
[Trait("Category", "CategoryName")]for categorization - Use collection fixtures to group tests with shared dependencies
- Skip tests conditionally with
Skip = "reason"in test attributes - Use XUnit v3's improved test discovery and filtering capabilities
Example with AAA Pattern
[Test]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
int a = 2;
int b = 3;
// Act
int result = calculator.Add(a, b);
// Assert
result.Should().Be(5);
}
[Theory]
[InlineData(0, 0, 0)]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
public void Add_VariousInputs_ReturnsExpectedResults(int a, int b, int expected)
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(a, b);
// Assert
result.Should().Be(expected);
}
// Example using new Observation attribute
[Observation]
public void Calculator_ShouldExist()
{
// Arrange & Act
var calculator = new Calculator();
// Assert
calculator.Should().NotBeNull();
}
// Example with test output
[Test]
public void Add_WithLogging_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
int a = 2;
int b = 3;
// Log test information
TestOutput.WriteLine($"Testing addition with values {a} and {b}");
// Act
int result = calculator.Add(a, b);
// Assert
result.Should().Be(5);
TestOutput.WriteLine($"Result was {result} as expected");
}
- Use async test methods for code that returns
Task:
[Fact]
public async Task SaveAsync_ShouldPersistData()
{
// Arrange
var repo = new Repository();
// Act
await repo.SaveAsync("data");
// Assert
var result = await repo.GetAsync();
result.Should().Be("data");
}