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:
- Build the use case with mocked collaborators.
- Arrange request inputs for both valid and invalid paths.
- Assert returned result or expected exception.
- 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.