unit-testing

Unit tests for Clean Architecture handlers:

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "unit-testing" with this command: npx skills add ronnythedev/dotnet-clean-architecture-skills/ronnythedev-dotnet-clean-architecture-skills-unit-testing

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;

/// &#x3C;summary>
/// Creates a mock that returns the provided result
/// &#x3C;/summary>
protected static T CreateMock&#x3C;T>() where T : class
{
    return Substitute.For&#x3C;T>();
}

/// &#x3C;summary>
/// Helper to create a successful Result
/// &#x3C;/summary>
protected static Result&#x3C;T> SuccessResult&#x3C;T>(T value)
{
    return Result.Success(value);
}

/// &#x3C;summary>
/// Helper to create a failed Result
/// &#x3C;/summary>
protected static Result&#x3C;T> FailureResult&#x3C;T>(Error error)
{
    return Result.Failure&#x3C;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&#x3C;I{Entity}Repository>();
    _unitOfWork = CreateMock&#x3C;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&#x3C;Domain.{Aggregate}.{Entity}>(e =>
            e.Name == command.Name &#x26;&#x26;
            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
        .GetByNameAsync(command.Name, CancellationToken)
        .Returns(existing{Entity});

    // Act
    await _handler.Handle(command, CancellationToken);

    // Assert
    _{entity}Repository
        .DidNotReceive()
        .Add(Arg.Any&#x3C;Domain.{Aggregate}.{Entity}>());
}

[Fact]
public async Task Handle_Should_NotCallSaveChanges_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
    await _handler.Handle(command, CancellationToken);

    // Assert
    await _unitOfWork
        .DidNotReceive()
        .SaveChangesAsync(Arg.Any&#x3C;CancellationToken>());
}

// ═══════════════════════════════════════════════════════════════
// HELPER METHODS
// ═══════════════════════════════════════════════════════════════

private static Domain.{Aggregate}.{Entity} CreateTest{Entity}(string name)
{
    // Use reflection or internal factory for testing
    // This assumes the entity has a factory method
    var result = Domain.{Aggregate}.{Entity}.Create(
        name,
        "Description",
        Guid.NewGuid());

    return result.Value;
}

}

Template: Query Handler Tests

// tests/{name}.Application.UnitTests/{Feature}/Get{Entity}ById/Get{Entity}ByIdQueryHandlerTests.cs using FluentAssertions; using NSubstitute; using {name}.application.{feature}.Get{Entity}ById; using {name}.application.abstractions.data; using {name}.Application.UnitTests.Abstractions;

namespace {name}.Application.UnitTests.{Feature}.Get{Entity}ById;

public sealed class Get{Entity}ByIdQueryHandlerTests : BaseTest { private readonly ISqlConnectionFactory _sqlConnectionFactory; private readonly Get{Entity}ByIdQueryHandler _handler;

public Get{Entity}ByIdQueryHandlerTests()
{
    _sqlConnectionFactory = CreateMock&#x3C;ISqlConnectionFactory>();
    _handler = new Get{Entity}ByIdQueryHandler(_sqlConnectionFactory);
}

[Fact]
public async Task Handle_Should_ReturnSuccess_When_EntityExists()
{
    // Arrange
    var entityId = Guid.NewGuid();
    var query = new Get{Entity}ByIdQuery(entityId);

    var expected = new {Entity}Response
    {
        Id = entityId,
        Name = "Test Entity",
        Description = "Description"
    };

    // Setup Dapper mock (simplified - in practice, mock IDbConnection)
    SetupConnectionToReturn(expected);

    // Act
    var result = await _handler.Handle(query, CancellationToken);

    // Assert
    result.IsSuccess.Should().BeTrue();
    result.Value.Id.Should().Be(entityId);
}

[Fact]
public async Task Handle_Should_ReturnFailure_When_EntityNotFound()
{
    // Arrange
    var entityId = Guid.NewGuid();
    var query = new Get{Entity}ByIdQuery(entityId);

    SetupConnectionToReturn(null);

    // Act
    var result = await _handler.Handle(query, CancellationToken);

    // Assert
    result.IsFailure.Should().BeTrue();
    result.Error.Should().Be({Entity}Errors.NotFound);
}

private void SetupConnectionToReturn({Entity}Response? response)
{
    // In practice, you'd use a library like Dapper.Contrib.Tests
    // or create a test double for IDbConnection
    // This is a simplified example
}

}

Template: Validator Tests

// tests/{name}.Application.UnitTests/{Feature}/Create{Entity}/Create{Entity}CommandValidatorTests.cs using FluentAssertions; using FluentValidation.TestHelper; using {name}.application.{feature}.Create{Entity}; using {name}.Application.UnitTests.Abstractions;

namespace {name}.Application.UnitTests.{Feature}.Create{Entity};

public sealed class Create{Entity}CommandValidatorTests : BaseTest { private readonly Create{Entity}CommandValidator _validator;

public Create{Entity}CommandValidatorTests()
{
    _validator = new Create{Entity}CommandValidator();
}

// ═══════════════════════════════════════════════════════════════
// NAME VALIDATION
// ═══════════════════════════════════════════════════════════════

[Fact]
public void Validate_Should_HaveError_When_NameIsEmpty()
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: string.Empty,
        Description: "Valid description",
        OrganizationId: Guid.NewGuid());

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldHaveValidationErrorFor(x => x.Name)
        .WithErrorMessage("{Entity} name is required");
}

[Fact]
public void Validate_Should_HaveError_When_NameTooLong()
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: new string('a', 101),  // Exceeds 100 char limit
        Description: "Valid description",
        OrganizationId: Guid.NewGuid());

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldHaveValidationErrorFor(x => x.Name)
        .WithErrorMessage("{Entity} name must not exceed 100 characters");
}

[Theory]
[InlineData("A")]
[InlineData("Valid Name")]
[InlineData("Name with 100 characters padded................................")]
public void Validate_Should_NotHaveError_When_NameIsValid(string name)
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: name,
        Description: "Valid description",
        OrganizationId: Guid.NewGuid());

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldNotHaveValidationErrorFor(x => x.Name);
}

// ═══════════════════════════════════════════════════════════════
// ORGANIZATION ID VALIDATION
// ═══════════════════════════════════════════════════════════════

[Fact]
public void Validate_Should_HaveError_When_OrganizationIdIsEmpty()
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: "Valid Name",
        Description: "Valid description",
        OrganizationId: Guid.Empty);

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldHaveValidationErrorFor(x => x.OrganizationId);
}

[Fact]
public void Validate_Should_NotHaveError_When_OrganizationIdIsValid()
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: "Valid Name",
        Description: "Valid description",
        OrganizationId: Guid.NewGuid());

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldNotHaveValidationErrorFor(x => x.OrganizationId);
}

// ═══════════════════════════════════════════════════════════════
// FULL VALIDATION
// ═══════════════════════════════════════════════════════════════

[Fact]
public void Validate_Should_BeValid_When_AllFieldsAreValid()
{
    // Arrange
    var command = new Create{Entity}Command(
        Name: "Valid Name",
        Description: "Valid description",
        OrganizationId: Guid.NewGuid());

    // Act
    var result = _validator.TestValidate(command);

    // Assert
    result.ShouldNotHaveAnyValidationErrors();
}

}

Template: Domain Entity Tests

// tests/{name}.Domain.UnitTests/{Aggregate}/{Entity}Tests.cs using FluentAssertions; using {name}.domain.{aggregate}; using {name}.domain.{aggregate}.events;

namespace {name}.Domain.UnitTests.{Aggregate};

public sealed class {Entity}Tests { // ═══════════════════════════════════════════════════════════════ // CREATE TESTS // ═══════════════════════════════════════════════════════════════

[Fact]
public void Create_Should_ReturnSuccess_When_ValidParameters()
{
    // Arrange
    var name = "Test Entity";
    var description = "Test Description";
    var organizationId = Guid.NewGuid();

    // Act
    var result = {Entity}.Create(name, description, organizationId);

    // Assert
    result.IsSuccess.Should().BeTrue();
    result.Value.Name.Should().Be(name);
    result.Value.OrganizationId.Should().Be(organizationId);
    result.Value.IsActive.Should().BeTrue();
}

[Fact]
public void Create_Should_ReturnFailure_When_NameIsEmpty()
{
    // Arrange
    var name = string.Empty;
    var description = "Test Description";
    var organizationId = Guid.NewGuid();

    // Act
    var result = {Entity}.Create(name, description, organizationId);

    // Assert
    result.IsFailure.Should().BeTrue();
    result.Error.Should().Be({Entity}Errors.NameRequired);
}

[Fact]
public void Create_Should_RaiseDomainEvent_When_Success()
{
    // Arrange
    var name = "Test Entity";
    var description = "Test Description";
    var organizationId = Guid.NewGuid();

    // Act
    var result = {Entity}.Create(name, description, organizationId);

    // Assert
    result.Value.GetDomainEvents()
        .Should().ContainSingle()
        .Which.Should().BeOfType&#x3C;{Entity}CreatedDomainEvent>();
}

// ═══════════════════════════════════════════════════════════════
// UPDATE TESTS
// ═══════════════════════════════════════════════════════════════

[Fact]
public void UpdateName_Should_ReturnSuccess_When_ValidName()
{
    // Arrange
    var entity = Create{Entity}();
    var newName = "Updated Name";

    // Act
    var result = entity.UpdateName(newName);

    // Assert
    result.IsSuccess.Should().BeTrue();
    entity.Name.Should().Be(newName);
}

[Fact]
public void UpdateName_Should_ReturnFailure_When_EmptyName()
{
    // Arrange
    var entity = Create{Entity}();

    // Act
    var result = entity.UpdateName(string.Empty);

    // Assert
    result.IsFailure.Should().BeTrue();
    result.Error.Should().Be({Entity}Errors.NameRequired);
}

// ═══════════════════════════════════════════════════════════════
// DEACTIVATE TESTS
// ═══════════════════════════════════════════════════════════════

[Fact]
public void Deactivate_Should_SetIsActiveToFalse()
{
    // Arrange
    var entity = Create{Entity}();
    entity.IsActive.Should().BeTrue();

    // Act
    entity.Deactivate();

    // Assert
    entity.IsActive.Should().BeFalse();
}

[Fact]
public void Deactivate_Should_RaiseDomainEvent()
{
    // Arrange
    var entity = Create{Entity}();
    entity.ClearDomainEvents();  // Clear create event

    // Act
    entity.Deactivate();

    // Assert
    entity.GetDomainEvents()
        .Should().ContainSingle()
        .Which.Should().BeOfType&#x3C;{Entity}DeactivatedDomainEvent>();
}

// ═══════════════════════════════════════════════════════════════
// HELPER METHODS
// ═══════════════════════════════════════════════════════════════

private static {Entity} Create{Entity}()
{
    var result = {Entity}.Create(
        "Test Entity",
        "Test Description",
        Guid.NewGuid());

    return result.Value;
}

}

Template: Test Data Builders

// tests/{name}.Application.UnitTests/TestData/{Entity}Builder.cs using {name}.application.{feature}.Create{Entity};

namespace {name}.Application.UnitTests.TestData;

public sealed class {Entity}CommandBuilder { private string _name = "Default Name"; private string _description = "Default Description"; private Guid _organizationId = Guid.NewGuid();

public {Entity}CommandBuilder WithName(string name)
{
    _name = name;
    return this;
}

public {Entity}CommandBuilder WithDescription(string description)
{
    _description = description;
    return this;
}

public {Entity}CommandBuilder WithOrganizationId(Guid organizationId)
{
    _organizationId = organizationId;
    return this;
}

public Create{Entity}Command Build()
{
    return new Create{Entity}Command(_name, _description, _organizationId);
}

}

// Usage in tests: // var command = new {Entity}CommandBuilder() // .WithName("Custom Name") // .Build();

NSubstitute Quick Reference

// Create mock var repository = Substitute.For<IRepository>();

// Setup return value repository.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()) .Returns(entity);

// Setup return null repository.GetByIdAsync(entityId, CancellationToken) .Returns((Entity?)null);

// Verify method was called repository.Received(1).Add(Arg.Any<Entity>());

// Verify method was NOT called repository.DidNotReceive().Add(Arg.Any<Entity>());

// Verify with argument matching repository.Received().Add(Arg.Is<Entity>(e => e.Name == "Test"));

// Verify call order (advanced) Received.InOrder(() => { repository.Add(Arg.Any<Entity>()); unitOfWork.SaveChangesAsync(CancellationToken); });

// Setup to throw exception repository.GetByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()) .ThrowsAsync(new Exception("Database error"));

FluentAssertions Quick Reference

// Basic assertions result.Should().BeTrue(); result.Should().BeFalse(); result.Should().BeNull(); result.Should().NotBeNull();

// Equality result.Should().Be(expected); result.Should().NotBe(unexpected); result.Should().BeEquivalentTo(expected);

// Collections list.Should().BeEmpty(); list.Should().NotBeEmpty(); list.Should().HaveCount(3); list.Should().Contain(item); list.Should().ContainSingle(); list.Should().ContainSingle().Which.Should().BeOfType<MyType>();

// Types result.Should().BeOfType<MyType>(); result.Should().BeAssignableTo<IMyInterface>();

// Strings name.Should().StartWith("Test"); name.Should().Contain("Entity"); name.Should().BeNullOrEmpty();

// Exceptions action.Should().Throw<InvalidOperationException>() .WithMessage("not found");

action.Should().NotThrow();

// Result pattern result.IsSuccess.Should().BeTrue(); result.Error.Should().Be(ExpectedError);

Critical Rules

  • One assert concept per test - Focus on single behavior

  • Descriptive test names - Should_{ExpectedBehavior}When{Condition}

  • Arrange-Act-Assert - Clear structure in every test

  • Mock only dependencies - Don't mock the SUT

  • Test behavior, not implementation - Focus on outcomes

  • Use Theory for data-driven tests - Avoid duplicate test logic

  • Test edge cases - Empty, null, boundaries

  • Fast tests - No I/O, no database

  • Independent tests - No shared state

  • Meaningful assertions - Test what matters

Anti-Patterns to Avoid

// ❌ WRONG: Multiple unrelated assertions [Fact] public void Test_Everything() { // Tests too many things at once result.IsSuccess.Should().BeTrue(); result.Value.Name.Should().Be("Test"); repository.Received(1).Add(Arg.Any<Entity>()); unitOfWork.Received(1).SaveChangesAsync(Arg.Any<CancellationToken>()); }

// ✅ CORRECT: Focused tests [Fact] public void Handle_Should_ReturnSuccess_When_ValidRequest() { }

[Fact] public void Handle_Should_AddEntity_When_ValidRequest() { }

[Fact] public void Handle_Should_CallSaveChanges_When_ValidRequest() { }

// ❌ WRONG: Testing implementation details repository.Received(1).GetByIdAsync(entityId, CancellationToken); repository.Received(1).Add(Arg.Any<Entity>()); // ... testing every single call

// ✅ CORRECT: Testing outcomes result.IsSuccess.Should().BeTrue(); result.Value.Id.Should().NotBeEmpty();

// ❌ WRONG: Shared mutable state private Entity _sharedEntity; // Modified by tests, causes flaky tests

// ✅ CORRECT: Fresh setup per test [Fact] public void Test() { var entity = CreateEntity(); // Fresh instance }

Related Skills

  • cqrs-command-generator

  • Commands to test

  • cqrs-query-generator

  • Queries to test

  • domain-entity-generator

  • Domain entities to test

  • integration-testing

  • End-to-end tests

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

dotnet-clean-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

repository-pattern

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

permission-authorization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

quartz-background-jobs

No summary provided by upstream source.

Repository SourceNeeds Review