Unit Testing Guide
This document explains the unit test writing rules and patterns for Functorium projects.
Introduction
Section titled “Introduction”“How should test method names be written to read consistently?”
“Why doesn’t the existing --filter option work in xUnit v3’s MTP mode?”
“What rules are needed to ensure multiple developers follow the same AAA pattern?”
Consistency in unit tests is just as important as code quality. As project size grows, if naming conventions, variable conventions, and framework settings are not unified, the test code itself becomes a maintenance burden.
What You Will Learn
Section titled “What You Will Learn”This document covers the following topics:
- T1_T2_T3 naming convention - How to structure test target, expected result, and scenario
- AAA pattern and standard variable names - Consistent variable conventions like
sut,actual,expected - MTP mode configuration and CLI options - Test execution and filtering in xUnit v3
- Shouldly-based Assertions - Validation patterns that provide clear failure messages
- [Fact] vs [Theory] selection criteria - Distinguishing between single-scenario and multi-input tests
Prerequisites
Section titled “Prerequisites”A basic understanding of the following concepts is needed to understand this document:
- C# asynchronous programming (
async/await,Task) - Basic concepts of the xUnit test framework (
[Fact],[Theory]) - .NET project build and execution (
dotnet build,dotnet test)
Core principle: Test code requires the same level of consistency as production code. Through the T1_T2_T3 naming convention and the AAA pattern, you can ensure team-wide consistency in both test method names and bodies.
Summary
Section titled “Summary”Key Commands (MTP Mode)
Section titled “Key Commands (MTP Mode)”# Run all testsdotnet test
# Test a specific projectdotnet test --project Tests/Functorium.Tests.Unit
# Run with code coverage (MTP mode)dotnet test -- --coverage --coverage-output-format cobertura
# Run specific tests only (MTP filter)dotnet test -- --filter-method "Handle_ReturnsSuccess"
# Class filteringdotnet test -- --filter-class "MyNamespace.MyTestClass"Note: In MTP mode, test options are passed after the
--separator.
Caution: In xUnit v3 (MTP mode), VSTest’s
--filteroption is not supported. Use--filter-class,--filter-method, etc. instead.
Key Procedures
Section titled “Key Procedures”1. Writing tests:
# 1. Create test class (Tests/{Project}.Tests.Unit/{Feature}/)# 2. Write test method (T1_T2_T3 naming convention)# 3. Apply AAA pattern (Arrange-Act-Assert)# 4. Run and verify tests2. Running tests:
# 1. Builddotnet build
# 2. Run testsdotnet test
# 3. Check resultsKey Concepts
Section titled “Key Concepts”1. Test Naming Convention (T1_T2_T3)
| Component | Description | Example |
|---|---|---|
| T1 | Test target method name | Validate, Handle |
| T2 | Expected result | ReturnsSuccess, ReturnsFail |
| T3 | Test scenario | WhenTitleIsEmpty |
2. AAA Pattern
| Phase | Variable Name | Description |
|---|---|---|
| Arrange | sut, request | Test preparation |
| Act | actual | Execution |
| Assert | - | Verification |
3. Test Packages
| Package | Purpose |
|---|---|
| xunit.v3 | Test framework |
| Microsoft.Testing.Extensions.CodeCoverage | Code coverage |
| Microsoft.Testing.Extensions.TrxReport | TRX report |
| Shouldly | Assertion library |
| NSubstitute | Mocking library |
| TngTech.ArchUnitNET.xUnitV3 | Architecture testing |
MTP Configuration
Section titled “MTP Configuration”MTP Configuration Details (click to expand)
What is Microsoft Testing Platform?
Section titled “What is Microsoft Testing Platform?”MTP (Microsoft Testing Platform) is the new test engine replacing VSTest. xUnit v3 natively supports MTP.
Activating MTP Mode
Section titled “Activating MTP Mode”To use MTP, both project settings and SDK version-specific settings are required.
Required Project Settings (common across all .NET versions)
Section titled “Required Project Settings (common across all .NET versions)”The following settings are required in the .csproj file of every test project:
<PropertyGroup> <OutputType>Exe</OutputType> <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner></PropertyGroup>| attribute | Description |
|---|---|
OutputType | Exe is required since MTP mode operates as a standalone executable (required in MTP mode, not needed in VSTest mode) |
UseMicrosoftTestingPlatformRunner | Enables MTP runner in xUnit v3 (xUnit-specific) |
Note: The reason for setting
OutputTypetoExeis a Microsoft official recommendation to prevent bugs during MSBuild/NuGet restore.
Tip: Adding common settings to a
Directory.Build.propsfile will automatically apply them to all test projects:<Project><PropertyGroup Condition="'$(IsTestProject)' == 'true'"><OutputType>Exe</OutputType><UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner></PropertyGroup></Project>
dotnet test Configuration by SDK Version
Section titled “dotnet test Configuration by SDK Version”.NET 10 SDK and above: Configure in global.json
{ "test": { "runner": "Microsoft.Testing.Platform" }}Location:
global.jsonis located at the solution root.Solution root/├── global.json ← MTP configuration location├── Directory.Packages.props└── Functorium.slnx
Note: With .NET 10 SDK and above, you can use MTP options directly without the
--separator.
.NET 8-9 SDK: Additional settings required in the project file
<PropertyGroup> <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport></PropertyGroup>Caution: In .NET 8-9, the
--separator is required when using thedotnet testcommand.
xUnit v3 MTP Package Selection
Section titled “xUnit v3 MTP Package Selection”xUnit v3 allows you to select the MTP version:
| Package | Description |
|---|---|
xunit.v3 | Default (includes MTP v1) |
xunit.v3.mtp-v1 | Explicitly specify MTP v1 |
xunit.v3.mtp-v2 | Use MTP v2 |
xunit.v3.mtp-off | Disable MTP (VSTest only) |
MTP CLI Options (xUnit v3)
Section titled “MTP CLI Options (xUnit v3)”| Feature | xUnit Native | MTP Command Line |
|---|---|---|
| Class filtering | -class "name" | --filter-class "name" |
| Method filtering | -method "name" | --filter-method "name" |
| Namespace filtering | -namespace "name" | --filter-namespace "name" |
| Trait filtering | -trait "name=value" | --filter-trait "name=value" |
| Parallel processing | -parallel <option> | --parallel <option> |
| HTML report | -html <file> | --report-xunit-html --report-xunit-html-filename <file> ¹ |
| JUnit report | -junit <file> | --report-junit --report-junit-filename <file> ¹ |
| Live output | -showLiveOutput | --show-live-output on |
¹ HTML/JUnit reports require separate package installation:
xunit.v3.reports.html,xunit.v3.reports.junit
VSTest vs MTP Filter Comparison
Section titled “VSTest vs MTP Filter Comparison”| Mode | Filter Option | Example |
|---|---|---|
| VSTest | --filter | dotnet test --filter "FullyQualifiedName~MyTest" |
| MTP | -- --filter-method | dotnet test -- --filter-method "MyTest" |
Note: In VSTest mode, the
--filteroption is used without the--separator.
Important: When using the
--filteroption in xUnit v3 (MTP mode), the following error occurs:Unknown option '--filter'In this case, use the
--filter-classor--filter-methodoptions instead.
Code Coverage Options (MTP)
Section titled “Code Coverage Options (MTP)”Available after installing the Microsoft.Testing.Extensions.CodeCoverage package:
| Option | Description |
|---|---|
--coverage | Enable code coverage (required) |
--coverage-output <file> | Specify output file name |
--coverage-output-format <format> | Format (coverage, xml, cobertura) |
--coverage-settings <file> | XML settings file path |
Usage examples:
# Collect coverage via dotnet testdotnet test -- --coverage --coverage-output-format cobertura --coverage-output coverage.xml
# Run directly via dotnet rundotnet run --project Tests -- --coverage --coverage-output-format coberturaTRX Report Options (MTP)
Section titled “TRX Report Options (MTP)”Available after installing the Microsoft.Testing.Extensions.TrxReport package:
| Option | Description |
|---|---|
--report-trx | Generate TRX report |
--report-trx-filename <file> | Specify output file name |
Usage examples:
# Generate TRX reportdotnet test -- --report-trx
# Specify file namedotnet test -- --report-trx --report-trx-filename results.trx
# Generate both coverage and TRX report (Build-Local.ps1 approach)dotnet test -- --coverage --coverage-output-format cobertura --coverage-output coverage.xml --report-trxOnce MTP settings and packages are ready, let’s look at the package configuration needed for test projects.
Test Packages
Section titled “Test Packages”| Package | Purpose | Note |
|---|---|---|
| xunit.v3 | Test framework | xUnit v3 (MTP-based) |
| xunit.runner.visualstudio | VS/IDE test explorer support | Required |
| Microsoft.NET.Test.Sdk | .NET test SDK | Required |
| Microsoft.Testing.Extensions.CodeCoverage | Code coverage collection | MTP extension |
| Microsoft.Testing.Extensions.TrxReport | TRX report generation | MTP extension |
| Shouldly | Fluent Assertion | Recommended |
| Verify.XunitV3 | Snapshot testing | For xUnit v3 |
| NSubstitute | Mocking | Recommended |
| TngTech.ArchUnitNET.xUnitV3 | Architecture testing | For xUnit v3 |
Package Installation
Section titled “Package Installation”# xUnit v3 (test framework) - required packagesdotnet add package xunit.v3dotnet add package xunit.runner.visualstudiodotnet add package Microsoft.NET.Test.Sdk
# MTP extensions (code coverage, TRX report)dotnet add package Microsoft.Testing.Extensions.CodeCoveragedotnet add package Microsoft.Testing.Extensions.TrxReport
# Shouldly (Assertion)dotnet add package Shouldly
# NSubstitute (Mocking)dotnet add package NSubstituteCaution: Without the
Microsoft.Testing.Extensions.TrxReportpackage, tests will not run when executingBuild-Local.ps1due to the--report-trxoption.
Once packages are ready, configure the test project’s csproj file.
Test Project Setup
Section titled “Test Project Setup”Basic csproj Configuration
Section titled “Basic csproj Configuration”Review the required MTP settings (OutputType, UseMicrosoftTestingPlatformRunner) and package reference configuration in the following csproj example.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> <!-- MTP required settings (details: see MTP Configuration section) --> <OutputType>Exe</OutputType> <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner> </PropertyGroup>
<ItemGroup> <!-- Required packages --> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio" />
<!-- MTP extensions (coverage, TRX report) --> <PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" /> <PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
<!-- Assertion library --> <PackageReference Include="Shouldly" /> </ItemGroup>
<ItemGroup> <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup>
<ItemGroup> <ProjectReference Include="..\MyProject\MyProject.csproj" /> </ItemGroup>
</Project>Important:
OutputTypeandUseMicrosoftTestingPlatformRunnerare required settings for MTP operation. For the role of each attribute and additional SDK version-specific settings, see the MTP Configuration section.
xunit.runner.json Configuration
Section titled “xunit.runner.json Configuration”Create an xunit.runner.json file at the test project root:
{ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "parallelizeAssembly": false, "parallelizeTestCollections": true}xUnit v3 Namespace Changes
Section titled “xUnit v3 Namespace Changes”When migrating from xUnit v2 to v3, the following namespace changes are required:
| v2 | v3 |
|---|---|
Xunit.Abstractions | Xunit |
ITestOutputHelper (Xunit.Abstractions) | ITestOutputHelper (Xunit) |
// xUnit v2using Xunit.Abstractions;
// xUnit v3using Xunit;Using xUnit Types in Non-Test Libraries
Section titled “Using xUnit Types in Non-Test Libraries”When you need to use xUnit types like ITestOutputHelper in a test utility library (e.g., Functorium.Testing):
<!-- Use xunit.v3.extensibility.core instead of xunit.v3 --><PackageReference Include="xunit.v3.extensibility.core" />Caution: The
xunit.v3package should only be used in test projects (<IsTestProject>true</IsTestProject>). Using it in non-test libraries will cause a “test projects must be executable” error.
Once project setup is complete, let’s look at the naming conventions that are central to writing test code.
Test Naming Convention
Section titled “Test Naming Convention”Test method names are written in the T1_T2_T3 format.
Format
Section titled “Format”{T1}_{T2}_{T3}| Component | Description | Example |
|---|---|---|
| T1 | Test target method name | Validate, Handle, Execute |
| T2 | Expected result | ReturnsSuccess, ReturnsFail, ThrowsException |
| T3 | Test scenario/condition | WhenTitleIsEmpty, WhenInputIsValid |
Validator Test Naming Examples
Section titled “Validator Test Naming Examples”// Return validation errorsValidate_ReturnsValidationError_WhenTitleIsEmptyValidate_ReturnsValidationError_WhenTitleExceedsMaxLengthValidate_ReturnsValidationError_WhenTemperatureCIsBelowMinimumValidate_ReturnsValidationError_WhenTemperatureCIsAboveMaximum
// Validation passesValidate_ReturnsNoError_WhenRequestIsValidValidate_ReturnsNoError_WhenTemperatureCIsWithinRangeValidate_ReturnsNoError_WhenTitleIsAtMaxLengthUsecase Test Naming Examples
Section titled “Usecase Test Naming Examples”// Success scenariosHandle_ReturnsSuccess_WhenTemperatureCIsPositiveHandle_ReturnsSuccess_WhenTemperatureCIsZeroHandle_ReturnsSuccess_WhenRequestIsValid
// Failure scenariosHandle_ReturnsFail_WhenTemperatureCIsNegativeHandle_ReturnsFail_WhenEntityNotFound
// Return value verificationHandle_ReturnsTemperatureCBasedOnTitleLength_WhenSuccessfulHandle_ReturnsTemperatureCEqualToTitleLength_WhenSuccessful[Fact] vs [Theory] Selection Criteria
Section titled “[Fact] vs [Theory] Selection Criteria”| Scenario | Attribute | Data Source | Example |
|---|---|---|---|
| Single scenario verification | [Fact] | None | Creation success, specific business rule |
| Same logic, multiple inputs | [Theory] + [InlineData] | Inline values | Boundary values, various valid/invalid inputs |
| Complex object inputs | [Theory] + [MemberData] | Static method/attribute | VO combinations, Entity state combinations |
T2 (Expected Result) Standard Terms
Section titled “T2 (Expected Result) Standard Terms”| Term | When to Use |
|---|---|
ReturnsSuccess | Returns a success result |
ReturnsFail | Returns a failure result |
ReturnsValidationError | Validation error |
ReturnsNoError | No error |
ThrowsException | Exception thrown |
Returns{Value} | Returns a specific value |
T3 (Scenario) Standard Prefixes
Section titled “T3 (Scenario) Standard Prefixes”| Prefix | When to Use | Example |
|---|---|---|
When | Condition/situation | WhenInputIsNull |
Given | Precondition | GivenUserIsAuthenticated |
With | Specific value | WithValidInput |
If the naming convention determines the “name” of a test method, then variable naming conventions determine the consistency of the test method “body.”
Variable Naming Convention
Section titled “Variable Naming Convention”Standard Variable Names
Section titled “Standard Variable Names”| Variable Name | Purpose | AAA Phase |
|---|---|---|
sut | System Under Test | Arrange |
request | Request object | Arrange |
actual | Execution result | Act |
expected | Expected result (for comparison) | Assert |
Usage Example
Section titled “Usage Example”[Fact]public async Task Handle_ReturnsSuccess_WhenRequestIsValid(){ // Arrange var sut = new MyUsecase(); var request = new MyRequest(Title: "Valid");
// Act var actual = await sut.Handle(request, CancellationToken.None);
// Assert actual.IsSucc.ShouldBeTrue();}AAA Pattern
Section titled “AAA Pattern”All tests follow the Arrange-Act-Assert pattern.
Structure
Section titled “Structure”Note that each phase is clearly separated by // Arrange, // Act, // Assert comments.
[Fact]public async Task T1_T2_T3(){ // Arrange - Test preparation var sut = new TestTarget(); var request = new Request(...);
// Act - Execution var actual = await sut.Method(request);
// Assert - Verification actual.ShouldBe(expected);}Complete Example
Section titled “Complete Example”[Fact]public async Task Handle_ReturnsSuccess_WhenTemperatureCIsPositive(){ // Arrange var sut = new UpdateWeatherForecastCommand.Usecase(); var request = new UpdateWeatherForecastCommand.Request( Title: "Valid Title", Description: "Valid description", TemperatureC: 25);
// Act var actual = await sut.Handle(request, CancellationToken.None);
// Assert actual.IsSucc.ShouldBeTrue();}Shouldly Assertion Examples
Section titled “Shouldly Assertion Examples”// Value comparisonactual.ShouldBe(expected);actual.ShouldNotBe(unexpected);
// Booleanactual.IsSucc.ShouldBeTrue();actual.IsFail.ShouldBeFalse();
// Null checksactual.ShouldBeNull();actual.ShouldNotBeNull();
// Collectionslist.ShouldBeEmpty();list.ShouldContain(item);list.Count.ShouldBe(3);
// ExceptionsShould.Throw<ArgumentException>(() => sut.Method());Troubleshooting
Section titled “Troubleshooting”When Tests Are Not Discovered
Section titled “When Tests Are Not Discovered”Cause: xUnit package version mismatch or Test SDK missing
Resolution:
# Check packagesdotnet list package
# Install required packagesdotnet add package xunit.v3dotnet add package Microsoft.NET.Test.Sdkdotnet add package xunit.runner.visualstudioWhen Some Tests Are Not Executed in Build-Local.ps1
Section titled “When Some Tests Are Not Executed in Build-Local.ps1”Cause: Missing Microsoft.Testing.Extensions.TrxReport package
Symptom:
- All tests pass when running directly with
dotnet test - Only some tests run with “Error: N” message when executing
Build-Local.ps1
Resolution:
# Add TrxReport packagedotnet add package Microsoft.Testing.Extensions.TrxReportOr add directly to the csproj file:
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />“test projects must be executable” Error
Section titled ““test projects must be executable” Error”Cause: Using the xunit.v3 package in a non-test library
Resolution: Use xunit.v3.extensibility.core instead of xunit.v3:
<!-- Incorrect setting (non-test library) --><PackageReference Include="xunit.v3" />
<!-- Correct setting (non-test library) --><PackageReference Include="xunit.v3.extensibility.core" />ITestOutputHelper Namespace Error (xUnit v3)
Section titled “ITestOutputHelper Namespace Error (xUnit v3)”Cause: Namespace change when migrating from xUnit v2 to v3
Resolution:
// Before (v2)using Xunit.Abstractions;
// After (v3)using Xunit;“Unknown option ‘—filter’” Error
Section titled ““Unknown option ‘—filter’” Error”Cause: VSTest’s --filter option is not supported in xUnit v3 (MTP mode)
Symptom:
Unknown option '--filter'Resolution:
Use the following filter options in MTP mode:
| VSTest Option | MTP Alternative | Example |
|---|---|---|
--filter "FullyQualifiedName~MyTest" | --filter-method "*MyTest*" | Method name filter |
--filter "ClassName~MyClass" | --filter-class "*MyClass*" | Class name filter |
--filter "Namespace~MyNamespace" | --filter-namespace "*MyNamespace*" | Namespace filter |
# Incorrect usage (not supported in MTP)dotnet test --filter "FullyQualifiedName~DomainEventPublisherTests"
# Correct usage (MTP)dotnet test --filter-class "*DomainEventPublisherTests"dotnet test --filter-method "*ReturnsSuccess*"Note: The
--separator is optional in .NET 10 SDK and above. In .NET 8-9, you must use the formatdotnet test -- --filter-class "...".
When Async Tests Fail
Section titled “When Async Tests Fail”Cause: Using async void or missing await
Resolution:
// Incorrect example[Fact]public async void Handle_ReturnsSuccess_WhenValid() // Using async void{ var actual = sut.Handle(request); // Missing await}
// Correct example[Fact]public async Task Handle_ReturnsSuccess_WhenValid() // Using async Task{ var actual = await sut.Handle(request); // Using await}When Shouldly Assertion Messages Are Unclear
Section titled “When Shouldly Assertion Messages Are Unclear”Cause: Using default Assert
Resolution:
// Unclear messageAssert.True(actual.IsSucc); // "Expected: True, Actual: False"
// Clear message (Shouldly)actual.IsSucc.ShouldBeTrue(); // "actual.IsSucc should be True but was False"When Mock Objects Don’t Behave as Expected
Section titled “When Mock Objects Don’t Behave as Expected”Cause: Missing NSubstitute setup
Resolution:
// Mock setupvar repository = Substitute.For<IRepository>();repository.GetById(Arg.Any<int>()).Returns(expectedEntity);
// Call verificationrepository.Received(1).GetById(42);Q1. What if the test method name becomes too long?
Section titled “Q1. What if the test method name becomes too long?”A: Maintain the T1_T2_T3 format, but write each part concisely:
// Too longHandle_ReturnsValidationErrorWithDetailedMessage_WhenUserInputTemperatureCelsiusValueIsNegativeNumber
// Appropriate nameHandle_ReturnsValidationError_WhenTemperatureCIsNegativeQ2. How do you test multiple conditions?
Section titled “Q2. How do you test multiple conditions?”A: Use [Theory] and [InlineData]:
[Theory][InlineData(-10)][InlineData(-1)][InlineData(int.MinValue)]public void Validate_ReturnsFail_WhenTemperatureCIsNegative(int temperature){ var request = new Request(TemperatureC: temperature); var actual = sut.Validate(request); actual.IsFail.ShouldBeTrue();}Q2-1. How do you use complex objects as inputs?
Section titled “Q2-1. How do you use complex objects as inputs?”A: Use [Theory] and [MemberData]. Since [InlineData] only supports primitive types, VO combinations and complex objects are provided from static methods via [MemberData]:
[Theory][MemberData(nameof(InvalidRequests))]public void Validate_ReturnsValidationError_WhenRequestIsInvalid( CreateProductCommand.Request request, string expectedErrorCode){ // Arrange var sut = new CreateProductCommand.Validator();
// Act var actual = sut.Validate(request);
// Assert actual.IsFail.ShouldBeTrue(); actual.FailToSeq().Head.Code.Value.ShouldBe(expectedErrorCode);}
public static IEnumerable<object[]> InvalidRequests(){ yield return new object[] { new CreateProductCommand.Request(Name: "", Price: 100m), "Validation.NameRequired" }; yield return new object[] { new CreateProductCommand.Request(Name: "Valid", Price: -1m), "Validation.PriceNegative" };}Tip: The data source method for
[MemberData]must bepublic staticand returnIEnumerable<object[]>.
Q3. How should test classes be organized?
Section titled “Q3. How should test classes be organized?”A: Create one test class per class under test:
Tests/Functorium.Tests.Unit/├── Features/│ └── WeatherForecast/│ ├── UpdateWeatherForecastCommandTests.cs│ └── GetWeatherForecastQueryTests.cs└── Common/ └── ValidationTests.csQ4. How do you test private methods?
Section titled “Q4. How do you test private methods?”A: Do not test private methods directly. Test them indirectly through public methods. If you need to test a private method directly, reconsider the design.
Q5. How do you test code with external dependencies (DB, API)?
Section titled “Q5. How do you test code with external dependencies (DB, API)?”A: Create mock objects using NSubstitute:
[Fact]public async Task Handle_ReturnsSuccess_WhenEntityExists(){ // Arrange var repository = Substitute.For<IRepository>(); repository.GetByIdAsync(42).Returns(new Entity { Id = 42 });
var sut = new MyUsecase(repository);
// Act var actual = await sut.Handle(new Request(Id: 42));
// Assert actual.IsSucc.ShouldBeTrue();}Q6. How do you handle tests that depend on execution order?
Section titled “Q6. How do you handle tests that depend on execution order?”A: Tests should be independent. Set up the required state directly in each test so they don’t depend on execution order:
// Bad Example - depends on other tests[Fact]public void Test2_DependsOnTest1(){ // Depends on state set by Test1}
// Good Example - independent[Fact]public void Test2_IsIndependent(){ // Arrange - Set up all required state directly var sut = CreateSut(); SetupRequiredState();
// Act & Assert}Q7. How do you check code coverage?
Section titled “Q7. How do you check code coverage?”A: Use the MTP code coverage extension:
# MTP coverage collection (recommended)dotnet test -- --coverage --coverage-output-format cobertura --coverage-output coverage.xml
# Generate report (requires ReportGenerator)reportgenerator -reports:"**/coverage.xml" -targetdir:"coverage-report"Or run the Build-Local.ps1 script, which automatically generates a coverage report.
Note: The VSTest approach (
--collect:"XPlat Code Coverage") still works, but the MTP approach is recommended.
Coverage Exclusion Settings (coverlet.runsettings)
Section titled “Coverage Exclusion Settings (coverlet.runsettings)”If there are targets to exclude from coverage, such as generated code or migrations, place a coverlet.runsettings file at the solution root:
<?xml version="1.0" encoding="utf-8" ?><RunSettings> <DataCollectionRunSettings> <DataCollectors> <DataCollector friendlyName="XPlat Code Coverage"> <Configuration> <ExcludeByAttribute> GeneratedCodeAttribute,CompilerGeneratedAttribute </ExcludeByAttribute> <ExcludeByFile> **/Migrations/*.cs,**/*.g.cs,**/*.Designer.cs </ExcludeByFile> <Exclude> [*.Tests.*]*,[*]*.Migrations.* </Exclude> </Configuration> </DataCollector> </DataCollectors> </DataCollectionRunSettings></RunSettings># Collect coverage with specified runsettings filedotnet test -- --coverage --coverage-settings coverlet.runsettingsAppendix: xUnit v3
Section titled “Appendix: xUnit v3”Theory Data Patterns
Section titled “Theory Data Patterns”InlineData
Section titled “InlineData”Passes constant values directly. Only supports primitive types (int, string, bool, etc.).
[Theory][InlineData(1, 2, 3)][InlineData(-5, 5, 0)]public void Add_ReturnsSum(int a, int b, int expected){ Assert.Equal(expected, new Calculator().Add(a, b));}MemberData
Section titled “MemberData”Retrieves data from static methods or attributes. Allows passing complex objects.
public static IEnumerable<object[]> AddTestData =>[ [1, 2, 3], [10, 20, 30],];
[Theory][MemberData(nameof(AddTestData))]public void Add_WithMemberData_ReturnsExpected(int a, int b, int expected){ Assert.Equal(expected, new Calculator().Add(a, b));}Note: The data source for
[MemberData]must bepublic staticand returnIEnumerable<object[]>.
ClassData
Section titled “ClassData”Provides data from a separate class.
public class AddTestData : IEnumerable<object[]>{ public IEnumerator<object[]> GetEnumerator() { yield return [1, 2, 3]; yield return [-5, 5, 0]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();}
[Theory][ClassData(typeof(AddTestData))]public void Add_WithClassData_ReturnsExpected(int a, int b, int expected) { }TheoryData (Strongly-Typed)
Section titled “TheoryData (Strongly-Typed)”A strongly-typed data source that provides type safety.
public static TheoryData<int, int, int> AddTestData => new(){ { 1, 2, 3 }, { -5, 5, 0 }};
[Theory][MemberData(nameof(AddTestData))]public void Add_ReturnsExpected(int a, int b, int expected) { }TheoryDataRow (v3 Row Metadata)
Section titled “TheoryDataRow (v3 Row Metadata)”In v3, you can specify metadata such as Skip, Timeout, and DisplayName on individual rows.
public static IEnumerable<ITheoryDataRow> GetTestData(){ yield return new TheoryDataRow<int, int, int>(1, 2, 3); yield return new TheoryDataRow<int, int, int>(10, 20, 30) { Skip = "Not yet implemented" }; yield return new TheoryDataRow<int, int, int>(-5, 5, 0) { Timeout = 1000, DisplayName = "Negative number test" };}MatrixTheoryData (v3 Combinations)
Section titled “MatrixTheoryData (v3 Combinations)”Automatically generates combinations of multiple data sets.
public static MatrixTheoryData<int, string> MatrixData => new( [1, 2, 3], ["A", "B", "C"]);// Result: (1,A), (1,B), (1,C), (2,A), (2,B), (2,C), (3,A), (3,B), (3,C)
[Theory][MemberData(nameof(MatrixData))]public void Matrix_AllCombinations(int number, string letter) { }v3 New Features
Section titled “v3 New Features”Dynamic Test Skipping
Section titled “Dynamic Test Skipping”[Fact]public void Test_SkipOnCondition(){ Assert.SkipWhen(!OperatingSystem.IsWindows(), "Only runs on Windows"); Assert.SkipUnless(OperatingSystem.IsWindows(), "Only runs on Windows"); Assert.Skip("Not yet implemented");}Explicit Tests
Section titled “Explicit Tests”Only runs when explicitly requested by the user.
[Fact(Explicit = true)]public void ManualIntegrationTest() { }TestContext
Section titled “TestContext”Accesses context information during test execution.
[Fact]public async Task Test_WithContext(){ var context = TestContext.Current; var cancellationToken = context.CancellationToken; context.SendDiagnosticMessage("Test started");}ITestContextAccessor (Dependency Injection)
Section titled “ITestContextAccessor (Dependency Injection)”public class MyTests(ITestContextAccessor contextAccessor){ [Fact] public void Test_WithInjectedContext() { var context = contextAccessor.Current; context.SendDiagnosticMessage("Dependency-injected context"); }}Assembly Fixture
Section titled “Assembly Fixture”Manages shared resources at the assembly level.
public class DatabaseFixture : IAsyncLifetime{ public string ConnectionString { get; private set; } = ""; public async ValueTask InitializeAsync() { /* Initialize */ } public async ValueTask DisposeAsync() { /* Cleanup */ }}
[assembly: AssemblyFixture(typeof(DatabaseFixture))]
public class DatabaseTests(DatabaseFixture fixture){ [Fact] public void Test() => Assert.NotEmpty(fixture.ConnectionString);}Console/Trace Output Capture
Section titled “Console/Trace Output Capture”[assembly: CaptureConsole][assembly: CaptureTrace]xunit.runner.json Key Settings
Section titled “xunit.runner.json Key Settings”{ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", "parallelizeAssembly": false, "parallelizeTestCollections": true, "maxParallelThreads": 4, "diagnosticMessages": true, "longRunningTestSeconds": 60, "methodDisplay": "classAndMethod", "methodDisplayOptions": "replaceUnderscoreWithSpace"}| Option | Default | Description |
|---|---|---|
parallelizeAssembly | false | Parallel execution across assemblies |
parallelizeTestCollections | true | Parallel execution across collections |
maxParallelThreads | CPU count | Maximum parallel thread count |
longRunningTestSeconds | 0 | Long-running test detection (0=disabled) |
methodDisplayOptions | none | replaceUnderscoreWithSpace, useOperatorMonikers, all |
preEnumerateTheories | true | Pre-enumerate Theory data |
failSkips | false | Treat skipped tests as failures |
References
Section titled “References”Functorium.Testing Library
Section titled “Functorium.Testing Library”- Structured log testing (LogTestContext), architecture rule validation, source generator testing, scheduled Job testing: 16-testing-library.md
- Error type Assertions (ShouldBeDomainError, etc.): 08b-error-system-domain-app.md, 08c-error-system-adapter-testing.md
xUnit v3
Section titled “xUnit v3”- xUnit.net v3 What’s New
- xUnit.net v3 Migration Guide
- xUnit.net v3 Microsoft Testing Platform
- xUnit.net v3 Code Coverage with MTP