Unit Test Generator
Overview
Unit tests for Clean Architecture handlers:
- xUnit - Test framework
- NSubstitute - Mocking library
- FluentAssertions - Readable assertions
- AAA pattern - Arrange, Act, Assert
Quick Reference
| Test Type | Purpose | Example |
|---|---|---|
| Success test | Verify happy path | Should_ReturnSuccess_When_ValidRequest |
| Failure test | Verify error handling | Should_ReturnFailure_When_NotFound |
| Validation test | Verify input validation | Should_ReturnValidationError_When_EmptyName |
| Behavior test | Verify side effects | Should_CallRepository_When_ValidRequest |
Test Project Structure
tests/
└── {name}.Application.UnitTests/
├── {Feature}/
│ ├── Create{Entity}/
│ │ ├── Create{Entity}CommandHandlerTests.cs
│ │ └── Create{Entity}CommandValidatorTests.cs
│ └── Get{Entity}ById/
│ └── Get{Entity}ByIdQueryHandlerTests.cs
├── Abstractions/
│ └── BaseTest.cs
└── {name}.Application.UnitTests.csproj
Template: Test Project File
<!-- tests/{name}.Application.UnitTests/{name}.Application.UnitTests.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.16">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.6.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\{name}.application\{name}.application.csproj" />
<ProjectReference Include="..\..\src\{name}.domain\{name}.domain.csproj" />
</ItemGroup>
</Project>
Template: Base Test Class
// tests/{name}.Application.UnitTests/Abstractions/BaseTest.cs
using NSubstitute;
using {name}.domain.abstractions;
namespace {name}.Application.UnitTests.Abstractions;
public abstract class BaseTest
{
protected static CancellationToken CancellationToken => CancellationToken.None;
/// <summary>
/// Creates a mock that returns the provided result
/// </summary>
protected static T CreateMock<T>() where T : class
{
return Substitute.For<T>();
}
/// <summary>
/// Helper to create a successful Result
/// </summary>
protected static Result<T> SuccessResult<T>(T value)
{
return Result.Success(value);
}
/// <summary>
/// Helper to create a failed Result
/// </summary>
protected static Result<T> FailureResult<T>(Error error)
{
return Result.Failure<T>(error);
}
}
Template: Command Handler Tests
// tests/{name}.Application.UnitTests/{Feature}/Create{Entity}/Create{Entity}CommandHandlerTests.cs
using FluentAssertions;
using NSubstitute;
using {name}.application.{feature}.Create{Entity};
using {name}.domain.{aggregate};
using {name}.domain.abstractions;
using {name}.Application.UnitTests.Abstractions;
namespace {name}.Application.UnitTests.{Feature}.Create{Entity};
public sealed class Create{Entity}CommandHandlerTests : BaseTest
{
private readonly I{Entity}Repository _{entity}Repository;
private readonly IUnitOfWork _unitOfWork;
private readonly Create{Entity}CommandHandler _handler;
public Create{Entity}CommandHandlerTests()
{
// Arrange - Setup mocks (runs before each test)
_{entity}Repository = CreateMock<I{Entity}Repository>();
_unitOfWork = CreateMock<IUnitOfWork>();
_handler = new Create{Entity}CommandHandler(
_{entity}Repository,
_unitOfWork);
}
// ═══════════════════════════════════════════════════════════════
// SUCCESS TESTS
// ═══════════════════════════════════════════════════════════════
[Fact]
public async Task Handle_Should_ReturnSuccess_When_ValidRequest()
{
// Arrange
var command = new Create{Entity}Command(
Name: "Test Entity",
Description: "Test Description",
OrganizationId: Guid.NewGuid());
_{entity}Repository
.GetByNameAsync(command.Name, CancellationToken)
.Returns((Domain.{Aggregate}.{Entity}?)null);
// Act
var result = await _handler.Handle(command, CancellationToken);
// Assert
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeEmpty();
}
[Fact]
public async Task Handle_Should_AddEntity_When_ValidRequest()
{
// Arrange
var command = new Create{Entity}Command(
Name: "Test Entity",
Description: "Test Description",
OrganizationId: Guid.NewGuid());
_{entity}Repository
.GetByNameAsync(command.Name, CancellationToken)
.Returns((Domain.{Aggregate}.{Entity}?)null);
// Act
await _handler.Handle(command, CancellationToken);
// Assert
_{entity}Repository
.Received(1)
.Add(Arg.Is<Domain.{Aggregate}.{Entity}>(e =>
e.Name == command.Name &&
e.OrganizationId == command.OrganizationId));
}
[Fact]
public async Task Handle_Should_CallSaveChanges_When_ValidRequest()
{
// Arrange
var command = new Create{Entity}Command(
Name: "Test Entity",
Description: "Test Description",
OrganizationId: Guid.NewGuid());
_{entity}Repository
.GetByNameAsync(command.Name, CancellationToken)
.Returns((Domain.{Aggregate}.{Entity}?)null);
// Act
await _handler.Handle(command, CancellationToken);
// Assert
await _unitOfWork
.Received(1)
.SaveChangesAsync(CancellationToken);
}
// ═══════════════════════════════════════════════════════════════
// FAILURE TESTS
// ═══════════════════════════════════════════════════════════════
[Fact]
public async Task Handle_Should_ReturnFailure_When_NameAlreadyExists()
{
// Arrange
var command = new Create{Entity}Command(
Name: "Existing Entity",
Description: "Test Description",
OrganizationId: Guid.NewGuid());
var existing{Entity} = CreateTest{Entity}(command.Name);
_{entity}Repository
.GetByNameAsync(command.Name, CancellationToken)
.Returns(existing{Entity});
// Act
var result = await _handler.Handle(command, CancellationToken);
// Assert
result.IsFailure.Should().BeTrue();
result.Error.Should().Be({Entity}Errors.NameAlreadyExists);
}
[Fact]
public async Task Handle_Should_NotAddEntity_When_NameAlreadyExists()
{
// Arrange
var command = new Create{Entity}Command(
Name: "Existing Entity",
Description: "Test Description",
OrganizationId: Guid.NewGuid());
var existing{Entity} = CreateTest{Entity}(command.Name);
_{entity}Repository
.G