Table of Contents

Unit Testing

Unit tests in the template focus on validating application and domain behavior in isolation (without real database or external infrastructure).

What DevKit already provides

The DevKit includes a testing library (CodeBlock.DevKit.Test) that the template test projects already reference.

The unit-test base class is UnitTestsBase in UnitTestsBase.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Test.TestBase;

/// <summary>
/// Base abstract class for unit test classes that provides common test lifecycle management.
/// Implements IDisposable to ensure proper cleanup after each test method execution.
/// Each derived test class must implement FixtureSetup to configure test-specific setup.
/// </summary>

public abstract class UnitTestsBase : IDisposable
{
    /// <summary>
    /// Initializes a new instance of UnitTestsBase and calls FixtureSetup.
    /// This constructor runs before each test method to ensure fresh test state.
    /// </summary>
    public UnitTestsBase()
    {
        FixtureSetup();
    }

    /// <summary>
    /// Abstract method that must be implemented by derived test classes.
    /// Called during construction to set up test-specific fixtures, mocks, and test data.
    /// This method runs before each test method to ensure isolated test execution.
    /// </summary>

    protected abstract void FixtureSetup();

    /// <summary>
    /// Disposal method called after each test method execution.
    /// Override this method in derived classes to perform custom cleanup operations.
    /// </summary>
    public void Dispose() { }
}

This gives you a simple, repeatable fixture lifecycle through FixtureSetup() for each test class.

The DevKit also provides reusable test data helpers in TestDataGenerator.cs.

// Copyright (c) CodeBlock.Dev. All rights reserved.
// For more information visit https://codeblock.dev

namespace CodeBlock.DevKit.Test.TestData;

/// <summary>
/// Utility class for generating random test data during unit and integration testing.
/// Provides methods to create random names, emails, and strings for populating test scenarios.
/// Uses a thread-safe random number generator for consistent test data generation.
/// </summary>

public static class TestDataGenerator
{
    /// <summary>
    /// Thread-safe random number generator for generating test data.
    /// </summary>
    private static readonly Random _random = new();

    /// <summary>
    /// Character set containing uppercase letters and numbers for generating random strings.
    /// </summary>
    private const string CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    /// <summary>
    /// Character set containing only uppercase letters for generating random names.
    /// </summary>
    private const string ALPHABETS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    /// <summary>
    /// Generates a random name string using only alphabetic characters.
    /// Useful for creating test usernames, display names, or other text fields that should not contain numbers.
    /// </summary>
    /// <param name="length">The length of the random name to generate.</param>
    /// <returns>A random string containing only uppercase letters.</returns>

    public static string GetRandomName(int length)
    {
        return new string(Enumerable.Repeat(ALPHABETS, length).Select(s => s[_random.Next(s.Length)]).ToArray());
    }

    /// <summary>
    /// Generates a random email address using a random name and a fixed domain.
    /// Creates realistic-looking test email addresses for testing user registration and authentication.
    /// </summary>
    /// <returns>A random email address in the format "[email protected]".</returns>

    public static string GetRandomEmail()
    {
        return $"{GetRandomName(10)}@canbeyours.online";
    }

    /// <summary>
    /// Generates a random string using alphanumeric characters (letters and numbers).
    /// Useful for creating test API keys, tokens, or other identifiers that can contain both letters and numbers.
    /// </summary>
    /// <param name="length">The length of the random string to generate.</param>
    /// <returns>A random string containing uppercase letters and numbers.</returns>

    public static string GetRandomString(int length)
    {
        return new string(Enumerable.Repeat(CHARS, length).Select(s => s[_random.Next(s.Length)]).ToArray());
    }
}

Use these helpers as a starting point, then shape your own test fixtures and data setup around your real domain.

Template fixture example

The template unit fixture setup is in TestsBaseFixture.cs.

using CanBeYours.Core.Domain.DemoThings;
using CodeBlock.DevKit.Application.Srvices;
using CodeBlock.DevKit.Test.TestBase;
using NSubstitute;

namespace CanBeYours.Application.Tests.Unit.Fixtures;

/// <summary>
/// Base test fixture for unit tests that provides common setup and mock dependencies.
/// This class demonstrates how to create a robust test infrastructure for unit testing
/// with mocked repositories, request dispatchers, and user context. The DemoThing functionality
/// shown here is just an example to help you learn how to implement your own unique features
/// into the current codebase. You can use this pattern to test your actual business logic.
/// </summary>
public abstract class TestsBaseFixture : UnitTestsBase
{
    /// <summary>
    /// Mock request dispatcher for testing domain event publishing
    /// </summary>
    protected IRequestDispatcher RequestDispatcher;
    
    /// <summary>
    /// Mock repository for DemoThing entities with in-memory test data
    /// </summary>
    protected IDemoThingRepository DemoThingRepository;
    
    /// <summary>
    /// In-memory list of DemoThing entities for testing repository operations
    /// </summary>
    protected List<DemoThing> DemoThings = new List<DemoThing>();
    
    /// <summary>
    /// Mock current user context for testing user-specific operations
    /// </summary>
    protected ICurrentUser CurrentUser;

    /// <summary>
    /// Sets up the test fixture by calling common setup and test-specific setup.
    /// This method is called by the base class to prepare the test environment.
    /// </summary>
    protected override void FixtureSetup()
    {
        CommonFixtureSetup();

        TestClassFixtureSetup();
    }

    /// <summary>
    /// Sets up common test dependencies including mocked services and test data.
    /// This method configures the mock repository with in-memory test data and
    /// sets up the request dispatcher and current user mocks.
    /// </summary>
    private void CommonFixtureSetup()
    {
        RequestDispatcher = Substitute.For<IRequestDispatcher>();
        CurrentUser = Substitute.For<ICurrentUser>();
        CurrentUser.GetUserId().Returns(Guid.NewGuid().ToString());

        DemoThings = GenerateDemoThingsList();

        DemoThingRepository = Substitute.For<IDemoThingRepository>();
        DemoThingRepository
            .GetByIdAsync(Arg.Is<string>(x => DemoThings.Any(o => o.Id == x)))
            .Returns(args => DemoThings.First(u => u.Id == (string)args[0]));
        DemoThingRepository
            .AddAsync(Arg.Any<DemoThing>())
            .Returns(args =>
            {
                DemoThings.Add((DemoThing)args[0]);
                return Task.CompletedTask;
            });
        DemoThingRepository
            .UpdateAsync(Arg.Is<DemoThing>(x => DemoThings.Any(o => o.Id == x.Id)))
            .Returns(args =>
            {
                var existDemoThing = DemoThings.FirstOrDefault(u => u.Id == ((DemoThing)args[0]).Id);
                if (existDemoThing != null)
                {
                    DemoThings.Remove(existDemoThing);
                    DemoThings.Add((DemoThing)args[0]);
                }

                return Task.CompletedTask;
            });


        DemoThingRepository
            .DeleteAsync(Arg.Is<string>(x => DemoThings.Any(o => o.Id == x)))
            .Returns(args =>
            {
                var demoThing = DemoThings.FirstOrDefault(u => u.Id == (string)args[0]);
                if (demoThing != null)
                    DemoThings.Remove(demoThing);

                return Task.CompletedTask;
            });
    }

    /// <summary>
    /// Abstract method that each test class must implement to set up its own specific fixture.
    /// This allows test classes to configure additional dependencies or test data as needed.
    /// </summary>
    protected abstract void TestClassFixtureSetup();

    /// <summary>
    /// Generates a list of sample DemoThing entities for testing purposes.
    /// This method creates test data that demonstrates different DemoThing types and configurations.
    /// Example usage: var testData = GenerateDemoThingsList();
    /// </summary>
    /// <returns>A list of DemoThing entities with different types and descriptions</returns>
    private List<DemoThing> GenerateDemoThingsList()
    {
        return new List<DemoThing>
        {
            DemoThing.Create("Demo Thing 1", "Description 1", DemoThingType.DemoType1, "User1"),
            DemoThing.Create("Demo Thing 2", "Description 2", DemoThingType.DemoType2, "User2"),
            DemoThing.Create("Demo Thing 3", "Description 3", DemoThingType.DemoType3, "User3"),
        };
    }
}

It shows how to compose mocks and in-memory test behavior for use-case testing.

One complete test example

For a complete unit test example, review CreateDemoThingTests.cs.

using CanBeYours.Application.Tests.Unit.Fixtures;
using CanBeYours.Application.UseCases.DemoThings.CreateDemoThing;
using CanBeYours.Core.Domain.DemoThings;
using CanBeYours.Core.Resources;
using CodeBlock.DevKit.Core.Exceptions;
using CodeBlock.DevKit.Core.Resources;
using CodeBlock.DevKit.Domain.Exceptions;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace CanBeYours.Application.Tests.Unit.UseCases.DemoThings;

/// <summary>
/// Unit tests for the CreateDemoThing use case that verify business logic and validation rules.
/// This test class demonstrates how to write comprehensive unit tests for command handlers
/// including success scenarios, validation failures, and domain event publishing. The DemoThing
/// functionality shown here is just an example to help you learn how to implement your own
/// unique features into the current codebase. You can use this testing pattern to verify your
/// actual business logic works correctly in isolation.
/// </summary>
public class CreateDemoThingTests : TestsBaseFixture
{
    /// <summary>
    /// The use case instance being tested
    /// </summary>
    private CreateDemoThingUseCase createDemoThingUseCase;

    /// <summary>
    /// Default constructor for the test class
    /// </summary>
    public CreateDemoThingTests() { }

    /// <summary>
    /// Tests that a DemoThing entity can be successfully created with valid input.
    /// This test verifies the happy path scenario where all required data is provided
    /// and the entity is created, persisted, and domain events are published.
    /// Example: Creating a new product, user, or any business entity with valid data.
    /// </summary>
    [Fact]
    public async Task DemoThing_is_created()
    {
        //Arrange
        var request = new CreateDemoThingRequest(name: "Test Name", description: "Test Description", type: DemoThingType.DemoType1);

        //Act
        var result = await createDemoThingUseCase.Handle(request, CancellationToken.None);

        //Assert
        result.EntityId.Should().NotBeNull();
        await RequestDispatcher!.Received(1).PublishEvent(Arg.Any<DemoThingCreated>());
    }

    /// <summary>
    /// Tests that a DemoThing entity cannot be created when the name is empty or null.
    /// This test verifies that the domain validation rules are enforced and appropriate
    /// domain exceptions are thrown for invalid input. Example: Attempting to create
    /// a user without a username or a product without a name.
    /// </summary>
    [Fact]
    public async Task DemoThing_is_not_created_if_name_is_not_provided()
    {
        //Arrange
        var expectedError = new DomainException(
            nameof(CoreResource.Required),
            typeof(CoreResource),
            new List<MessagePlaceholder> { MessagePlaceholder.CreateResource(SharedResource.DemoThing_Name, typeof(SharedResource)) }
        );
        var existedDemoThing = DemoThings.FirstOrDefault();
        var request = new CreateDemoThingRequest(name: "", "Test_Description", DemoThingType.DemoType3);

        //Act
        Func<Task> act = async () => await createDemoThingUseCase.Handle(request, CancellationToken.None);

        //Assert
        await act.Should().ThrowAsync<DomainException>().Where(e => e.Message.Equals(expectedError.Message));
    }

    /// <summary>
    /// Tests that a DemoThing entity cannot be created when the description is empty or null.
    /// This test verifies that the domain validation rules are enforced and appropriate
    /// domain exceptions are thrown for invalid input. Example: Attempting to create
    /// a product without a description or a document without content.
    /// </summary>
    [Fact]
    public async Task DemoThing_is_not_created_if_description_is_not_provided()
    {
        //Arrange
        var expectedError = new DomainException(
            nameof(CoreResource.Required),
            typeof(CoreResource),
            new List<MessagePlaceholder> { MessagePlaceholder.CreateResource(SharedResource.DemoThing_Description, typeof(SharedResource)) }
        );
        var existedDemoThing = DemoThings.FirstOrDefault();
        var request = new CreateDemoThingRequest(name: "Test Name", description: "", type: DemoThingType.DemoType3);

        //Act
        Func<Task> act = async () => await createDemoThingUseCase.Handle(request, CancellationToken.None);

        //Assert
        await act.Should().ThrowAsync<DomainException>().Where(e => e.Message.Equals(expectedError.Message));
    }

    /// <summary>
    /// Sets up the test-specific fixture by creating the use case instance with mocked dependencies.
    /// This method is called by the base fixture setup to prepare the specific test environment.
    /// Example: Creating a service instance with mocked repositories and external dependencies.
    /// </summary>
    protected override void TestClassFixtureSetup()
    {
        var logger = Substitute.For<ILogger<CreateDemoThingUseCase>>();

        createDemoThingUseCase = new CreateDemoThingUseCase(DemoThingRepository, RequestDispatcher, logger, CurrentUser);
    }
}

Use this as a reference for use-case-level test design:

  1. Build the use case with mocked collaborators.
  2. Arrange request inputs for both valid and invalid paths.
  3. Assert returned result or expected exception.
  4. Verify side effects such as repository calls and dispatched events.

When you add your own feature, create tests at the same use-case level and adapt the fixture/mocks to your feature dependencies.

The template pattern is a recommended baseline, not a requirement. You can use your own testing architecture if it better fits your team.