dotnet-add-testing
Add test infrastructure scaffolding to an existing .NET project. Creates test projects with xUnit, configures code
coverage with coverlet, and sets up the conventional directory structure.
Scope
- Test project creation with xUnit and coverlet
- Conventional directory structure (tests/ mirroring src/)
- Project reference wiring and test SDK configuration
Out of scope
- In-depth testing patterns (xUnit v3, WebApplicationFactory, UI testing) -- see [skill:dotnet-testing-strategy]
Prerequisites: Run [skill:dotnet-version-detection] first to determine SDK version and TFM. Run
[skill:dotnet-project-analysis] to understand existing solution structure.
Cross-references: [skill:dotnet-project-structure] for overall solution layout conventions,
[skill:dotnet-scaffold-project] which includes test scaffolding in new projects, [skill:dotnet-add-analyzers] for
test-specific analyzer suppressions.
Test Project Structure
Follow the convention of mirroring src/ project names under tests/:
MyApp/
├── src/
│ ├── MyApp.Core/
│ ├── MyApp.Api/
│ └── MyApp.Infrastructure/
└── tests/
├── MyApp.Core.UnitTests/
├── MyApp.Api.UnitTests/
├── MyApp.Api.IntegrationTests/
└── Directory.Build.props # Test-specific build settings
```xml
Naming conventions:
- `*.UnitTests` -- isolated tests with no external dependencies
- `*.IntegrationTests` -- tests that use real infrastructure (database, HTTP, file system)
- `*.FunctionalTests` -- end-to-end tests through the full application stack
---
## Step 1: Create the Test Project
```bash
# Create xUnit test project
dotnet new xunit -n MyApp.Core.UnitTests -o tests/MyApp.Core.UnitTests
# Add to solution
dotnet sln add tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj
# Add reference to the project under test
dotnet add tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj \
reference src/MyApp.Core/MyApp.Core.csproj
```csharp
### Clean Up Generated Project
Remove properties already defined in `Directory.Build.props`:
```xml
<!-- tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
</Project>
```csharp
With CPM, `Version` attributes are managed in `Directory.Packages.props`. Remove them from the generated `.csproj`.
---
## Step 2: Add Test-Specific Build Properties
Create `tests/Directory.Build.props` to customize settings for all test projects:
```xml
<!-- tests/Directory.Build.props -->
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- Use Microsoft.Testing.Platform v2 runner (requires Microsoft.NET.Test.Sdk 17.13+/18.x) -->
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
<!-- Relax strictness for test projects -->
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
```text
This imports the root `Directory.Build.props` (for shared settings like `Nullable`, `ImplicitUsings`, `LangVersion`) and
overrides test-specific properties.
---
## Step 3: Register Test Packages in CPM
Add test package versions to `Directory.Packages.props`:
```xml
<!-- In Directory.Packages.props -->
<ItemGroup>
<!-- Test packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
</ItemGroup>
```text
### Optional: Mocking Library
Add a mocking library if the project needs test doubles:
```xml
<PackageVersion Include="NSubstitute" Version="5.3.0" />
```xml
Or for assertion libraries:
```xml
<PackageVersion Include="FluentAssertions" Version="8.0.1" />
```xml
---
## Step 4: Configure Code Coverage
### Coverlet (Collector Mode)
The `coverlet.collector` package integrates with `dotnet test` via the data collector. No additional configuration is
needed for basic coverage.
Generate coverage reports:
```bash
# Collect coverage (Cobertura format by default)
dotnet test --collect:"XPlat Code Coverage"
# Results appear in TestResults/*/coverage.cobertura.xml
```xml
### Coverage Thresholds
For CI enforcement, use `coverlet.msbuild` for threshold checks:
```xml
<!-- In test csproj or tests/Directory.Build.props -->
<PackageReference Include="coverlet.msbuild" />
```xml
```bash
# Enforce minimum coverage threshold
dotnet test /p:CollectCoverage=true \
/p:CoverageOutputFormat=cobertura \
/p:Threshold=80 \
/p:ThresholdType=line
```text
### Coverage Report Generation
Use `reportgenerator` for human-readable HTML reports:
```bash
# Install globally
dotnet tool install -g dotnet-reportgenerator-globaltool
# Generate HTML report
reportgenerator \
-reports:"tests/**/coverage.cobertura.xml" \
-targetdir:coverage-report \
-reporttypes:Html
```xml
---
## Step 5: Add EditorConfig Overrides for Tests
In the root `.editorconfig`, add test-specific relaxations:
```ini
[tests/**.cs]
# Allow underscores in test method names (Given_When_Then or Should_Behavior)
dotnet_diagnostic.CA1707.severity = none
# Test parameters are validated by the framework
dotnet_diagnostic.CA1062.severity = none
# ConfigureAwait not relevant in test context
dotnet_diagnostic.CA2007.severity = none
# Tests often have intentionally unused variables for assertions
dotnet_diagnostic.IDE0059.severity = suggestion
```text
---
## Step 6: Write a Starter Test
Replace the template-generated `UnitTest1.cs` with a properly structured test:
```csharp
namespace MyApp.Core.UnitTests;
public class SampleServiceTests
{
[Fact]
public void Method_Condition_ExpectedResult()
{
// Arrange
var sut = new SampleService();
// Act
var result = sut.DoWork();
// Assert
Assert.NotNull(result);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Add_TwoNumbers_ReturnsSum(int a, int b, int expected)
{
var result = Calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
```text
### Test Naming Convention
Use the pattern `Method_Condition_ExpectedResult`:
- `CreateUser_WithValidInput_ReturnsUser`
- `GetById_WhenNotFound_ReturnsNull`
- `Delete_WithoutPermission_ThrowsUnauthorized`
---
## Verify
After adding test infrastructure, verify everything works:
```bash
# Restore (regenerate lock files if using CPM)
dotnet restore
# Build (verifies project references and analyzer config)
dotnet build --no-restore
# Run tests
dotnet test --no-build
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
```text
---
## Adding Integration Test Projects
For integration tests that need `WebApplicationFactory` or database access:
```bash
dotnet new xunit -n MyApp.Api.IntegrationTests -o tests/MyApp.Api.IntegrationTests
dotnet sln add tests/MyApp.Api.IntegrationTests/MyApp.Api.IntegrationTests.csproj
dotnet add tests/MyApp.Api.IntegrationTests/MyApp.Api.IntegrationTests.csproj \
reference src/MyApp.Api/MyApp.Api.csproj
```csharp
Add integration test packages to CPM (match the `Microsoft.AspNetCore.Mvc.Testing` major version to the target framework
-- e.g., `8.x` for `net8.0`, `9.x` for `net9.0`, `10.x` for `net10.0`):
```xml
<!-- Version must match the project's target framework major version -->
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Testcontainers" Version="4.3.0" />
```xml
Integration test depth (WebApplicationFactory patterns, test containers, database fixtures) -- see
[skill:dotnet-integration-testing].
---
## What's Next
This skill covers test project scaffolding. For deeper testing guidance:
- **xUnit v3 features and patterns** -- [skill:dotnet-xunit]
- **Integration testing with WebApplicationFactory** -- [skill:dotnet-integration-testing]
- **UI testing (Blazor, MAUI, Uno)** -- [skill:dotnet-blazor-testing], [skill:dotnet-maui-testing],
[skill:dotnet-uno-testing]
- **Snapshot testing** -- [skill:dotnet-snapshot-testing]
- **Test quality and coverage enforcement** -- [skill:dotnet-test-quality]
- **CI test reporting** -- [skill:dotnet-add-ci] for starter, [skill:dotnet-gha-build-test] and
[skill:dotnet-ado-build-test] for advanced
---
## Code Navigation (Serena MCP)
**Primary approach:** Use Serena symbol operations for efficient code navigation:
1. **Find definitions**: `serena_find_symbol` instead of text search
2. **Understand structure**: `serena_get_symbols_overview` for file organization
3. **Track references**: `serena_find_referencing_symbols` for impact analysis
4. **Precise edits**: `serena_replace_symbol_body` for clean modifications
**When to use Serena vs traditional tools:**
- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits
- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations
- ✅ **Fallback**: If Serena unavailable, traditional tools work fine
**Example workflow:**
```text
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
```
## References
- [xUnit Documentation](https://xunit.net/)
- [Coverlet](https://github.com/coverlet-coverage/coverlet)
- [.NET Testing Best Practices](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices)
- [Microsoft.NET.Test.Sdk](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk)
- [ReportGenerator](https://github.com/danielpalme/ReportGenerator)
1---2name: dotnet-add-testing3category: testing4subcategory: fundamentals5description: Adds test infrastructure to a .NET project. Scaffolds xUnit project, coverlet, layout.6license: MIT7targets: ['*']8tags: [testing, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for testing tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-add-testing2425Add test infrastructure scaffolding to an existing .NET project. Creates test projects with xUnit, configures code26coverage with coverlet, and sets up the conventional directory structure.2728## Scope2930- Test project creation with xUnit and coverlet31- Conventional directory structure (tests/ mirroring src/)32- Project reference wiring and test SDK configuration3334## Out of scope3536- In-depth testing patterns (xUnit v3, WebApplicationFactory, UI testing) -- see [skill:dotnet-testing-strategy]3738**Prerequisites:** Run [skill:dotnet-version-detection] first to determine SDK version and TFM. Run39[skill:dotnet-project-analysis] to understand existing solution structure.4041Cross-references: [skill:dotnet-project-structure] for overall solution layout conventions,42[skill:dotnet-scaffold-project] which includes test scaffolding in new projects, [skill:dotnet-add-analyzers] for43test-specific analyzer suppressions.4445---4647## Test Project Structure4849Follow the convention of mirroring `src/` project names under `tests/`:5051````text5253MyApp/54├── src/55│ ├── MyApp.Core/56│ ├── MyApp.Api/57│ └── MyApp.Infrastructure/58└── tests/59 ├── MyApp.Core.UnitTests/60 ├── MyApp.Api.UnitTests/61 ├── MyApp.Api.IntegrationTests/62 └── Directory.Build.props # Test-specific build settings6364```xml6566Naming conventions:6768- `*.UnitTests` -- isolated tests with no external dependencies69- `*.IntegrationTests` -- tests that use real infrastructure (database, HTTP, file system)70- `*.FunctionalTests` -- end-to-end tests through the full application stack7172---7374## Step 1: Create the Test Project7576```bash7778# Create xUnit test project79dotnet new xunit -n MyApp.Core.UnitTests -o tests/MyApp.Core.UnitTests8081# Add to solution82dotnet sln add tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj8384# Add reference to the project under test85dotnet add tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj \86 reference src/MyApp.Core/MyApp.Core.csproj8788```csharp8990### Clean Up Generated Project9192Remove properties already defined in `Directory.Build.props`:9394```xml9596<!-- tests/MyApp.Core.UnitTests/MyApp.Core.UnitTests.csproj -->97<Project Sdk="Microsoft.NET.Sdk">98 <ItemGroup>99 <PackageReference Include="Microsoft.NET.Test.Sdk" />100 <PackageReference Include="xunit.v3" />101 <PackageReference Include="xunit.runner.visualstudio" />102 <PackageReference Include="coverlet.collector" />103 </ItemGroup>104 <ItemGroup>105 <ProjectReference Include="..\..\src\MyApp.Core\MyApp.Core.csproj" />106 </ItemGroup>107</Project>108109```csharp110111With CPM, `Version` attributes are managed in `Directory.Packages.props`. Remove them from the generated `.csproj`.112113---114115## Step 2: Add Test-Specific Build Properties116117Create `tests/Directory.Build.props` to customize settings for all test projects:118119```xml120121<!-- tests/Directory.Build.props -->122<Project>123 <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />124 <PropertyGroup>125 <IsPackable>false</IsPackable>126 <IsTestProject>true</IsTestProject>127 <!-- Use Microsoft.Testing.Platform v2 runner (requires Microsoft.NET.Test.Sdk 17.13+/18.x) -->128 <UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>129 <!-- Relax strictness for test projects -->130 <TreatWarningsAsErrors>false</TreatWarningsAsErrors>131 </PropertyGroup>132</Project>133134```text135136This imports the root `Directory.Build.props` (for shared settings like `Nullable`, `ImplicitUsings`, `LangVersion`) and137overrides test-specific properties.138139---140141## Step 3: Register Test Packages in CPM142143Add test package versions to `Directory.Packages.props`:144145```xml146147<!-- In Directory.Packages.props -->148<ItemGroup>149 <!-- Test packages -->150 <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />151 <PackageVersion Include="xunit.v3" Version="3.2.2" />152 <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />153 <PackageVersion Include="coverlet.collector" Version="8.0.0" />154</ItemGroup>155156```text157158### Optional: Mocking Library159160Add a mocking library if the project needs test doubles:161162```xml163164<PackageVersion Include="NSubstitute" Version="5.3.0" />165166```xml167168Or for assertion libraries:169170```xml171172<PackageVersion Include="FluentAssertions" Version="8.0.1" />173174```xml175176---177178## Step 4: Configure Code Coverage179180### Coverlet (Collector Mode)181182The `coverlet.collector` package integrates with `dotnet test` via the data collector. No additional configuration is183needed for basic coverage.184185Generate coverage reports:186187```bash188189# Collect coverage (Cobertura format by default)190dotnet test --collect:"XPlat Code Coverage"191192# Results appear in TestResults/*/coverage.cobertura.xml193194```xml195196### Coverage Thresholds197198For CI enforcement, use `coverlet.msbuild` for threshold checks:199200```xml201202<!-- In test csproj or tests/Directory.Build.props -->203<PackageReference Include="coverlet.msbuild" />204205```xml206207```bash208209# Enforce minimum coverage threshold210dotnet test /p:CollectCoverage=true \211 /p:CoverageOutputFormat=cobertura \212 /p:Threshold=80 \213 /p:ThresholdType=line214215```text216217### Coverage Report Generation218219Use `reportgenerator` for human-readable HTML reports:220221```bash222223# Install globally224dotnet tool install -g dotnet-reportgenerator-globaltool225226# Generate HTML report227reportgenerator \228 -reports:"tests/**/coverage.cobertura.xml" \229 -targetdir:coverage-report \230 -reporttypes:Html231232```xml233234---235236## Step 5: Add EditorConfig Overrides for Tests237238In the root `.editorconfig`, add test-specific relaxations:239240```ini241242[tests/**.cs]243# Allow underscores in test method names (Given_When_Then or Should_Behavior)244dotnet_diagnostic.CA1707.severity = none245246# Test parameters are validated by the framework247dotnet_diagnostic.CA1062.severity = none248249# ConfigureAwait not relevant in test context250dotnet_diagnostic.CA2007.severity = none251252# Tests often have intentionally unused variables for assertions253dotnet_diagnostic.IDE0059.severity = suggestion254255```text256257---258259## Step 6: Write a Starter Test260261Replace the template-generated `UnitTest1.cs` with a properly structured test:262263```csharp264265namespace MyApp.Core.UnitTests;266267public class SampleServiceTests268{269 [Fact]270 public void Method_Condition_ExpectedResult()271 {272 // Arrange273 var sut = new SampleService();274275 // Act276 var result = sut.DoWork();277278 // Assert279 Assert.NotNull(result);280 }281282 [Theory]283 [InlineData(1, 2, 3)]284 [InlineData(0, 0, 0)]285 [InlineData(-1, 1, 0)]286 public void Add_TwoNumbers_ReturnsSum(int a, int b, int expected)287 {288 var result = Calculator.Add(a, b);289 Assert.Equal(expected, result);290 }291}292293```text294295### Test Naming Convention296297Use the pattern `Method_Condition_ExpectedResult`:298299- `CreateUser_WithValidInput_ReturnsUser`300- `GetById_WhenNotFound_ReturnsNull`301- `Delete_WithoutPermission_ThrowsUnauthorized`302303---304305## Verify306307After adding test infrastructure, verify everything works:308309```bash310311# Restore (regenerate lock files if using CPM)312dotnet restore313314# Build (verifies project references and analyzer config)315dotnet build --no-restore316317# Run tests318dotnet test --no-build319320# Run with coverage321dotnet test --collect:"XPlat Code Coverage"322323```text324325---326327## Adding Integration Test Projects328329For integration tests that need `WebApplicationFactory` or database access:330331```bash332333dotnet new xunit -n MyApp.Api.IntegrationTests -o tests/MyApp.Api.IntegrationTests334dotnet sln add tests/MyApp.Api.IntegrationTests/MyApp.Api.IntegrationTests.csproj335dotnet add tests/MyApp.Api.IntegrationTests/MyApp.Api.IntegrationTests.csproj \336 reference src/MyApp.Api/MyApp.Api.csproj337338```csharp339340Add integration test packages to CPM (match the `Microsoft.AspNetCore.Mvc.Testing` major version to the target framework341-- e.g., `8.x` for `net8.0`, `9.x` for `net9.0`, `10.x` for `net10.0`):342343```xml344345<!-- Version must match the project's target framework major version -->346<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />347<PackageVersion Include="Testcontainers" Version="4.3.0" />348349```xml350351Integration test depth (WebApplicationFactory patterns, test containers, database fixtures) -- see352[skill:dotnet-integration-testing].353354---355356## What's Next357358This skill covers test project scaffolding. For deeper testing guidance:359360- **xUnit v3 features and patterns** -- [skill:dotnet-xunit]361- **Integration testing with WebApplicationFactory** -- [skill:dotnet-integration-testing]362- **UI testing (Blazor, MAUI, Uno)** -- [skill:dotnet-blazor-testing], [skill:dotnet-maui-testing],363 [skill:dotnet-uno-testing]364- **Snapshot testing** -- [skill:dotnet-snapshot-testing]365- **Test quality and coverage enforcement** -- [skill:dotnet-test-quality]366- **CI test reporting** -- [skill:dotnet-add-ci] for starter, [skill:dotnet-gha-build-test] and367 [skill:dotnet-ado-build-test] for advanced368369---370371372373## Code Navigation (Serena MCP)374375**Primary approach:** Use Serena symbol operations for efficient code navigation:3763771. **Find definitions**: `serena_find_symbol` instead of text search3782. **Understand structure**: `serena_get_symbols_overview` for file organization3793. **Track references**: `serena_find_referencing_symbols` for impact analysis3804. **Precise edits**: `serena_replace_symbol_body` for clean modifications381382**When to use Serena vs traditional tools:**383- ✅ **Use Serena**: Navigation, refactoring, dependency analysis, precise edits384- ✅ **Use Read/Grep**: Reading full files, pattern matching, simple text operations385- ✅ **Fallback**: If Serena unavailable, traditional tools work fine386387**Example workflow:**388```text389# Instead of:390Read: src/Services/OrderService.cs391Grep: "public void ProcessOrder"392393# Use:394serena_find_symbol: "OrderService/ProcessOrder"395serena_get_symbols_overview: "src/Services/OrderService.cs"396```397## References398399- [xUnit Documentation](https://xunit.net/)400- [Coverlet](https://github.com/coverlet-coverage/coverlet)401- [.NET Testing Best Practices](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices)402- [Microsoft.NET.Test.Sdk](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk)403- [ReportGenerator](https://github.com/danielpalme/ReportGenerator)404````