Skip to content

Unit Testing in ConnectSoft Microservice Template

Purpose & Overview

Unit Testing is the practice of testing individual components in isolation to verify they work correctly. In the ConnectSoft Microservice Template, unit tests validate business logic, validators, mappers, processors, and other components without dependencies on external systems, databases, or network resources. Unit tests are fast, deterministic, and provide immediate feedback during development.

Unit testing provides:

  • Fast Feedback: Tests execute in milliseconds, providing immediate validation
  • Isolation: Components tested in isolation without external dependencies
  • Reliability: Deterministic tests that don't depend on external systems
  • Confidence: Verify code correctness before integration
  • Documentation: Tests serve as executable documentation of component behavior
  • Refactoring Safety: Confidence to refactor code knowing tests will catch regressions
  • Design Validation: Encourages testable, well-designed code

Unit Testing Philosophy

Unit tests validate that individual components work correctly in isolation. They should be fast, deterministic, and focused on testing a single unit of functionality. The template uses MSTest as the testing framework and Moq for mocking dependencies, following the AAA (Arrange-Act-Assert) pattern for clear, maintainable tests. Every component with business logic should have corresponding unit tests.

Architecture Overview

Unit Testing Stack

Unit Test
Test Framework (MSTest)
    ├── TestClass Attribute
    ├── TestMethod Attribute
    ├── TestInitialize/TestCleanup
    └── Assertions
Component Under Test
    ├── Domain Logic (Processors, Validators)
    ├── Mappers (AutoMapper)
    ├── Utilities (Helpers, Extensions)
    └── Domain Services
Test Doubles (Moq)
    ├── Mocks (Behavior Verification)
    ├── Stubs (Data Provision)
    ├── Fakes (Simplified Implementations)
    └── Spies (Interaction Recording)

Test Project Structure

UnitTests Project
├── DomainModel/
│   └── Validators/
│       ├── CreateMicroserviceAggregateRootInputValidatorUnitTests.cs
│       └── DeleteMicroserviceAggregateRootInputValidatorUnitTests.cs
├── ServiceModelInputValidation/
│   ├── CreateMicroserviceAggregateRootRequestValidationUnitTests.cs
│   └── GetMicroserviceAggregateRootDetailsRequestValidationUnitTests.cs
├── AutoMapper/
│   └── MicroserviceServiceModelMappingProfileUnitTests.cs
├── BotModel/
│   ├── MicroserviceTemplateBotControllerUnitTests.cs
│   └── MicroserviceTemplateDialogTests.cs
├── OrleansActorModel/
│   └── BankAccountActorTests.cs
├── Metrics/
│   ├── MicroserviceTemplateMetricsUnitTests.cs
│   └── FeatureAMetricsUnitTests.cs
└── Helpers/
    ├── TestDataBuilder.cs
    └── TestDataFactory.cs

Testing Framework

MSTest

MSTest is the primary testing framework used in the template.

Key Attributes:

  • [TestClass]: Marks a class as containing test methods
  • [TestMethod]: Marks a method as a test
  • [TestInitialize]: Method executed before each test
  • [TestCleanup]: Method executed after each test
  • [TestCategory]: Categorizes tests for filtering
  • [DataTestMethod]: Parameterized test method
  • [DataRow]: Provides data for parameterized tests

Example:

[TestClass]
public class MyComponentUnitTests
{
    private MyComponent component;

    [TestInitialize]
    public void Setup()
    {
        // Arrange: Setup test data
        this.component = new MyComponent();
    }

    [TestCleanup]
    public void Cleanup()
    {
        // Cleanup resources
        this.component?.Dispose();
    }

    [TestMethod]
    public void MyComponent_WithValidInput_Should_Succeed()
    {
        // Arrange
        var input = "test";

        // Act
        var result = this.component.Process(input);

        // Assert
        Assert.IsNotNull(result);
    }
}

Moq

Moq is used for creating test doubles (mocks, stubs, fakes).

Key Features: - Mock Creation: new Mock<T>() - Setup: .Setup() for configuring behavior - Verification: .Verify() for interaction verification - Returns: .Returns() and .ReturnsAsync() for return values

Example:

// Create mock
var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();

// Setup behavior
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
    .ReturnsAsync(new MicroserviceAggregateRootEntity { ObjectId = testId });

// Verify interactions
mockRepository.Verify(r => r.GetByIdAsync(testId), Times.Once);

For detailed information on mocking, see Mocking Strategies.

Test Structure

AAA Pattern

The AAA (Arrange-Act-Assert) pattern provides a clear structure for unit tests:

Arrange: Set up test data and dependencies Act: Execute the code under test Assert: Verify the expected outcome

Example:

[TestMethod]
public void CreateMicroserviceAggregateRootInputValidatorShouldPassValidInput()
{
    // Arrange
    var validator = new CreateMicroserviceAggregateRootInputValidator();
    var input = new CreateMicroserviceAggregateRootInput
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    ValidationResult result = validator.Validate(input);

    // Assert
    Assert.IsTrue(result.IsValid);
}

Test Naming Convention

Pattern: {Component}_{Scenario}_{ExpectedResult}

Examples: - CreateMicroserviceAggregateRootInputValidatorShouldPassValidInput - CreateMicroserviceAggregateRootInputValidatorShouldThrowExceptionWhenObjectIdIsEmptyGuid - MyComponent_WithValidInput_Should_ReturnSuccess - MyComponent_WithInvalidInput_Should_ThrowException

Characteristics: - Descriptive: Test name describes what is being tested - Readable: Clear scenario and expected outcome - Consistent: Follows naming pattern across all tests

Test Method Structure

Standard Structure:

/// <summary>
/// Brief description of what the test validates.
/// </summary>
[TestMethod]
public void ComponentNameShouldBehaviorWhenCondition()
{
    // Arrange
    // Setup test data, mocks, and dependencies

    // Act
    // Execute the code under test

    // Assert
    // Verify expected outcomes
}

Testing Different Components

Testing Validators

FluentValidation Validators:

[TestClass]
public class CreateMicroserviceAggregateRootInputValidatorUnitTests
{
    private readonly CreateMicroserviceAggregateRootInputValidator validator = new();

    [TestMethod]
    public void CreateMicroserviceAggregateRootInputValidatorShouldPassValidInput()
    {
        // Arrange
        var input = GetValidInput();

        // Act
        ValidationResult result = this.validator.Validate(input);

        // Assert
        Assert.IsTrue(result.IsValid);
    }

    [TestMethod]
    public void CreateMicroserviceAggregateRootInputValidatorShouldThrowExceptionWhenObjectIdIsEmptyGuid()
    {
        // Arrange
        var input = GetValidInput();
        input.ObjectId = Guid.Empty;

        // Act & Assert
        Assert.ThrowsExactly<ObjectIdRequiredException>(
            () => this.validator.Validate(input));
    }

    private static CreateMicroserviceAggregateRootInput GetValidInput()
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };
    }
}

DataAnnotations Validators:

[TestClass]
public class CreateMicroserviceAggregateRootRequestValidationUnitTests
{
    [TestMethod]
    public void CreateMicroserviceAggregateRootRequestShouldPassValidationWhenModelIsValid()
    {
        // Arrange
        var request = new CreateMicroserviceAggregateRootRequest
        {
            ObjectId = Guid.NewGuid()
        };

        // Act
        var results = ServiceModelInputValidationHelper.ValidateModel(request);

        // Assert
        Assert.AreEqual(0, results.Count);
    }

    [TestMethod]
    public void CreateMicroserviceAggregateRootRequestShouldFailValidationWhenObjectIdIsEmptyGuid()
    {
        // Arrange
        var request = new CreateMicroserviceAggregateRootRequest
        {
            ObjectId = Guid.Empty
        };

        // Act
        var results = ServiceModelInputValidationHelper.ValidateModel(request);

        // Assert
        Assert.AreEqual(1, results.Count);
        Assert.AreEqual(
            expected: string.Format(
                CultureInfo.InvariantCulture, 
                NotDefaultAttribute.DefaultErrorMessage, 
                nameof(CreateMicroserviceAggregateRootRequest.ObjectId)),
            actual: results[0].ErrorMessage);
    }
}

Testing Processors

Domain Processors with Mocked Dependencies:

[TestClass]
public class MicroserviceAggregateRootsProcessorUnitTests
{
    private Mock<IMicroserviceAggregateRootsRepository> mockRepository;
    private Mock<IUnitOfWork> mockUnitOfWork;
    private Mock<IEventBus> mockEventBus;
    private DefaultMicroserviceAggregateRootsProcessor processor;

    [TestInitialize]
    public void Setup()
    {
        this.mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
        this.mockUnitOfWork = new Mock<IUnitOfWork>();
        this.mockEventBus = new Mock<IEventBus>();

        this.processor = new DefaultMicroserviceAggregateRootsProcessor(
            Mock.Of<ILogger<DefaultMicroserviceAggregateRootsProcessor>>(),
            TimeProvider.System,
            this.mockRepository.Object,
            this.mockUnitOfWork.Object,
            this.mockEventBus.Object,
            Mock.Of<IValidator<CreateMicroserviceAggregateRootInput>>(),
            Mock.Of<IValidator<DeleteMicroserviceAggregateRootInput>>(),
            Mock.Of<MicroserviceTemplateMetrics>());
    }

    [TestMethod]
    public async Task Create_WhenAlreadyExists_ShouldThrowException()
    {
        // Arrange
        var input = new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };

        this.mockRepository.Setup(r => r.GetByIdAsync(input.ObjectId))
            .ReturnsAsync(new MicroserviceAggregateRootEntity 
            { 
                ObjectId = input.ObjectId 
            });

        // Act & Assert
        await Assert.ThrowsExceptionAsync<MicroserviceAggregateRootAlreadyExistsException>(
            () => this.processor.CreateMicroserviceAggregateRoot(input));
    }

    [TestMethod]
    public async Task Create_WithValidInput_Should_Succeed()
    {
        // Arrange
        var input = new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };

        this.mockRepository.Setup(r => r.GetByIdAsync(input.ObjectId))
            .ReturnsAsync((IMicroserviceAggregateRoot?)null);

        this.mockRepository.Setup(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()))
            .Verifiable();

        // Act
        var result = await this.processor.CreateMicroserviceAggregateRoot(input);

        // Assert
        Assert.IsNotNull(result);
        Assert.AreEqual(input.ObjectId, result.ObjectId);
        this.mockRepository.Verify(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), Times.Once);
    }
}

Testing Mappers

AutoMapper Configuration Validation:

[TestClass]
public class MicroserviceServiceModelMappingProfileUnitTests
{
    private MapperConfiguration? mapperConfiguration;

    [TestInitialize]
    public void InitializeMapperConfiguration()
    {
        // Arrange
        this.mapperConfiguration = new MapperConfiguration(cfg =>
        {
#if UseGrpc || UseRestApi || UseCoreWCF || UseGraphQL || UseServiceFabric || UseAzureFunction
            cfg.AddProfile<MicroserviceServiceModelMappingProfile>();
#endif
        });
    }

    [TestMethod]
    public void ValidateMicroserviceServiceModelMappingProfileMappings()
    {
        // Assert
        Assert.IsNotNull(this.mapperConfiguration);

        // Validates all mappings are configured correctly
        this.mapperConfiguration.AssertConfigurationIsValid();
    }
}

Mapping Execution Tests:

[TestMethod]
public void Map_RequestToInput_ShouldMapCorrectly()
{
    // Arrange
    var mapper = this.mapperConfiguration.CreateMapper();
    var request = new CreateMicroserviceAggregateRootRequest
    {
        ObjectId = Guid.NewGuid()
    };

    // Act
    var input = mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);

    // Assert
    Assert.IsNotNull(input);
    Assert.AreEqual(request.ObjectId, input.ObjectId);
}

Testing Controllers

Controller Tests with Fake Dependencies:

[TestClass]
public class MicroserviceTemplateBotControllerUnitTests
{
    [TestMethod]
    public async Task PostAsyncDelegatesToAdapterProcessAsync()
    {
        // Arrange
        var adapter = new FakeAdapter();
        var bot = new FakeBot();
        var loggerFactory = NullLoggerFactory.Instance;
        var controller = new MicroserviceTemplateBotController(loggerFactory, adapter, bot)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext()
            }
        };

        // Act
        await controller.PostAsync(CancellationToken.None);

        // Assert
        Assert.IsTrue(adapter.Processed, "Adapter.ProcessAsync should have been invoked.");
    }

    // Fake implementations for testing
    private class FakeAdapter : IBotFrameworkHttpAdapter
    {
        public bool Processed { get; private set; }

        public async Task ProcessAsync(HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default)
        {
            this.Processed = true;
            await Task.CompletedTask;
        }
    }

    private class FakeBot : IBot
    {
        public Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
            => Task.CompletedTask;
    }
}

Testing Actors (Orleans)

Orleans Actor Tests:

[TestClass]
public class BankAccountActorTests
{
    private TestCluster? cluster;

    [TestInitialize]
    public void TestInitialize()
    {
        this.cluster = new TestClusterBuilder(2)
            .AddClientBuilderConfigurator<TestClientBuilderConfigurator>()
            .AddSiloBuilderConfigurator<TestSiloConfigurator>()
            .Build();
        this.cluster.Deploy();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        this.cluster?.StopAllSilos();
    }

    [TestMethod]
    public async Task WithdrawWithValidInputShouldCompletesSuccessfully()
    {
        // Arrange
        var grain = this.cluster?.GrainFactory.GetGrain<IBankAccountGrain>(Guid.NewGuid());
        var input = new WithdrawInput { Amount = 100 };

        Assert.IsNotNull(grain);

        // Act
        WithdrawOutput output = await grain.Withdraw(input);

        // Assert
        Assert.IsNotNull(output);
        Assert.AreEqual(input.Amount, output.Amount);
    }
}

Test Data Management

Helper Methods

Private Helper Methods:

[TestClass]
public class ValidatorUnitTests
{
    private readonly MyValidator validator = new();

    [TestMethod]
    public void ValidatorShouldPassValidInput()
    {
        // Arrange
        var input = GetValidInput(); // Helper method

        // Act
        var result = this.validator.Validate(input);

        // Assert
        Assert.IsTrue(result.IsValid);
    }

    private static MyInput GetValidInput()
    {
        return new MyInput
        {
            ObjectId = Guid.NewGuid()
        };
    }
}

Test Data Builders

Fluent Builder Pattern:

public class CreateMicroserviceAggregateRootInputBuilder
{
    private Guid objectId = Guid.NewGuid();

    public CreateMicroserviceAggregateRootInputBuilder WithObjectId(Guid id)
    {
        this.objectId = id;
        return this;
    }

    public CreateMicroserviceAggregateRootInput Build()
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = this.objectId
        };
    }
}

// Usage
[TestMethod]
public void Processor_WithSpecificId_Should_Work()
{
    // Arrange
    var input = new CreateMicroserviceAggregateRootInputBuilder()
        .WithObjectId(Guid.Parse("11111111-1111-1111-1111-111111111111"))
        .Build();

    // Act & Assert
}

Test Data Factories

Reusable Factory Methods:

public static class TestDataFactory
{
    public static CreateMicroserviceAggregateRootInput CreateValidInput()
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };
    }

    public static CreateMicroserviceAggregateRootInput CreateInputWithId(Guid id)
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = id
        };
    }
}

// Usage
[TestMethod]
public void Processor_WithValidInput_Should_Succeed()
{
    // Arrange
    var input = TestDataFactory.CreateValidInput();

    // Act & Assert
}

For detailed information on test data management, see Test Data Management.

Assertions

Standard Assertions

MSTest Assertions:

// Equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(expected, actual);

// Null checks
Assert.IsNull(actual);
Assert.IsNotNull(actual);

// Boolean
Assert.IsTrue(condition);
Assert.IsFalse(condition);

// Type checks
Assert.IsInstanceOfType(actual, typeof(ExpectedType));

// Exceptions
Assert.ThrowsException<ExpectedException>(() => action());
Assert.ThrowsExceptionAsync<ExpectedException>(async () => await actionAsync());
Assert.ThrowsExactly<ExpectedException>(() => action());

// Collections
CollectionAssert.AreEqual(expected, actual);
CollectionAssert.Contains(collection, item);

Example:

[TestMethod]
public void ValidatorShouldPassValidInput()
{
    // Arrange
    var input = GetValidInput();

    // Act
    var result = this.validator.Validate(input);

    // Assert
    Assert.IsTrue(result.IsValid);
    Assert.AreEqual(0, result.Errors.Count);
}

[TestMethod]
public void ValidatorShouldFailWithEmptyGuid()
{
    // Arrange
    var input = GetValidInput();
    input.ObjectId = Guid.Empty;

    // Act & Assert
    Assert.ThrowsExactly<ObjectIdRequiredException>(
        () => this.validator.Validate(input));
}

Custom Assertions

Helper Methods for Complex Assertions:

[TestClass]
public class ProcessorUnitTests
{
    private void AssertEntityCreated(IMicroserviceAggregateRoot entity, Guid expectedId)
    {
        Assert.IsNotNull(entity);
        Assert.AreEqual(expectedId, entity.ObjectId);
        Assert.IsTrue(entity.CreatedAt > DateTime.UtcNow.AddMinutes(-1));
    }

    [TestMethod]
    public async Task Create_Should_CreateEntity()
    {
        // Arrange
        var input = TestDataFactory.CreateValidInput();

        // Act
        var result = await this.processor.CreateMicroserviceAggregateRoot(input);

        // Assert
        this.AssertEntityCreated(result, input.ObjectId);
    }
}

Test Organization

Test Class Organization

One Test Class Per Component:

UnitTests/
├── DomainModel/
│   └── Validators/
│       └── CreateMicroserviceAggregateRootInputValidatorUnitTests.cs
├── ServiceModelInputValidation/
│   └── CreateMicroserviceAggregateRootRequestValidationUnitTests.cs
└── AutoMapper/
    └── MicroserviceServiceModelMappingProfileUnitTests.cs

Naming Convention: - Test class name: {ComponentName}UnitTests - Test class mirrors the component being tested - Grouped by namespace/feature area

Test Categories

Categorizing Tests:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
    // Architecture test
}

[TestMethod]
[TestCategory("Unit Tests")]
public void ValidatorShouldPassValidInput()
{
    // Unit test
}

Usage: - Filter tests by category in test runners - Run specific test categories in CI/CD - Organize tests by purpose (Unit, Integration, Architecture)

Test Initialization and Cleanup

TestInitialize:

[TestClass]
public class ProcessorUnitTests
{
    private Mock<IRepository> mockRepository;
    private Processor processor;

    [TestInitialize]
    public void Setup()
    {
        // Arrange: Setup fresh state for each test
        this.mockRepository = new Mock<IRepository>();
        this.processor = new Processor(this.mockRepository.Object);
    }
}

TestCleanup:

[TestClass]
public class ActorTests
{
    private TestCluster? cluster;

    [TestInitialize]
    public void TestInitialize()
    {
        this.cluster = new TestClusterBuilder().Build();
        this.cluster.Deploy();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        // Cleanup resources
        this.cluster?.StopAllSilos();
    }
}

Testing Best Practices

Do's

  1. Follow AAA Pattern

    [TestMethod]
    public void Component_Scenario_ExpectedResult()
    {
        // Arrange
        var input = TestDataFactory.CreateValidInput();
    
        // Act
        var result = this.component.Process(input);
    
        // Assert
        Assert.IsNotNull(result);
    }
    

  2. Use Descriptive Test Names

    // ✅ GOOD - Clear and descriptive
    public void CreateMicroserviceAggregateRootInputValidatorShouldPassValidInput()
    public void CreateMicroserviceAggregateRootInputValidatorShouldThrowExceptionWhenObjectIdIsEmptyGuid()
    
    // ❌ BAD - Vague
    public void Test1()
    public void ValidatorTest()
    

  3. Test One Thing Per Test

    // ✅ GOOD - One scenario per test
    [TestMethod]
    public void ValidatorShouldPassValidInput()
    
    [TestMethod]
    public void ValidatorShouldFailWithEmptyGuid()
    
    // ❌ BAD - Multiple scenarios in one test
    [TestMethod]
    public void ValidatorShouldHandleAllCases()
    

  4. Use Helper Methods for Test Data

    // ✅ GOOD - Reusable helper
    private static CreateMicroserviceAggregateRootInput GetValidInput()
    {
        return new CreateMicroserviceAggregateRootInput
        {
            ObjectId = Guid.NewGuid()
        };
    }
    

  5. Mock External Dependencies

    // ✅ GOOD - Mocked dependencies
    var mockRepository = new Mock<IRepository>();
    var processor = new Processor(mockRepository.Object);
    

  6. Test Edge Cases

    // ✅ GOOD - Test edge cases
    [TestMethod]
    public void ValidatorShouldFailWithEmptyGuid()
    
    [TestMethod]
    public void ValidatorShouldFailWithNullInput()
    

Don'ts

  1. Don't Test Implementation Details

    // ❌ BAD - Testing internal implementation
    [TestMethod]
    public void ProcessorShouldCallRepositoryInsert()
    {
        // Testing internal method calls
    }
    
    // ✅ GOOD - Testing behavior
    [TestMethod]
    public void Processor_WithValidInput_Should_CreateEntity()
    {
        // Testing public behavior
    }
    

  2. Don't Use Real External Dependencies

    // ❌ BAD - Real database/repository
    var repository = new RealRepository(connectionString);
    
    // ✅ GOOD - Mocked dependency
    var mockRepository = new Mock<IRepository>();
    

  3. Don't Share State Between Tests

    // ❌ BAD - Shared state
    private static List<Entity> sharedEntities = new();
    
    // ✅ GOOD - Isolated state
    private List<Entity> testEntities = new();
    

  4. Don't Ignore Async/Await

    // ❌ BAD - Missing await
    var result = this.processor.ProcessAsync(input);
    
    // ✅ GOOD - Proper async/await
    var result = await this.processor.ProcessAsync(input);
    

  5. Don't Use Hardcoded Values

    // ❌ BAD - Hardcoded values
    var id = Guid.Parse("12345678-1234-1234-1234-123456789012");
    
    // ✅ GOOD - Generated or builder-based
    var id = Guid.NewGuid();
    var input = new InputBuilder().WithObjectId(id).Build();
    

  6. Don't Skip Assertions

    // ❌ BAD - No assertions
    [TestMethod]
    public void Test()
    {
        var result = this.component.Process();
        // No assertions
    }
    
    // ✅ GOOD - Clear assertions
    [TestMethod]
    public void Test()
    {
        var result = this.component.Process();
        Assert.IsNotNull(result);
        Assert.AreEqual(expected, result.Value);
    }
    

Test Coverage

Coverage Goals

Recommended Coverage: - Domain Logic: 80-90% coverage - Validators: 100% coverage (all validation rules) - Mappers: 100% coverage (all mappings) - Utilities: 80-90% coverage - Infrastructure: 60-70% coverage (focus on critical paths)

Coverage Tools

Coverage Tools: - Visual Studio: Built-in code coverage - Coverlet: Cross-platform code coverage - dotCover: JetBrains coverage tool - SonarQube: Code quality and coverage analysis

Configuration:

<ItemGroup>
  <PackageReference Include="coverlet.collector" Version="6.0.0">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>

Parameterized Tests

DataTestMethod

Parameterized Tests with DataRow:

[DataTestMethod]
[DataRow("valid@email.com", true)]
[DataRow("invalid-email", false)]
[DataRow("", false)]
[DataRow(null, false)]
public void EmailValidator_WithVariousInputs_Should_ValidateCorrectly(string email, bool expected)
{
    // Arrange
    var validator = new EmailValidator();

    // Act
    var result = validator.Validate(email);

    // Assert
    Assert.AreEqual(expected, result.IsValid);
}

Async Testing

Async Test Methods

Async Test Methods:

[TestMethod]
public async Task Processor_WithValidInput_Should_Succeed()
{
    // Arrange
    var input = TestDataFactory.CreateValidInput();

    // Act
    var result = await this.processor.CreateMicroserviceAggregateRoot(input);

    // Assert
    Assert.IsNotNull(result);
}

[TestMethod]
public async Task Processor_WhenAlreadyExists_Should_ThrowException()
{
    // Arrange
    var input = TestDataFactory.CreateValidInput();
    this.mockRepository.Setup(r => r.GetByIdAsync(input.ObjectId))
        .ReturnsAsync(new Entity());

    // Act & Assert
    await Assert.ThrowsExceptionAsync<AlreadyExistsException>(
        () => this.processor.CreateMicroserviceAggregateRoot(input));
}

Important: Always use async Task (not async void) for async test methods.

Exception Testing

Testing Exceptions

Synchronous Exceptions:

[TestMethod]
public void Validator_WithInvalidInput_Should_ThrowException()
{
    // Arrange
    var input = GetValidInput();
    input.ObjectId = Guid.Empty;

    // Act & Assert
    Assert.ThrowsExactly<ObjectIdRequiredException>(
        () => this.validator.Validate(input));
}

Asynchronous Exceptions:

[TestMethod]
public async Task Processor_WhenAlreadyExists_Should_ThrowException()
{
    // Arrange
    var input = TestDataFactory.CreateValidInput();
    this.mockRepository.Setup(r => r.GetByIdAsync(input.ObjectId))
        .ReturnsAsync(new Entity());

    // Act & Assert
    await Assert.ThrowsExceptionAsync<AlreadyExistsException>(
        () => this.processor.CreateMicroserviceAggregateRoot(input));
}

Troubleshooting

Issue: Tests Pass Individually but Fail Together

Symptoms: Tests pass when run individually but fail when run as a suite.

Solutions: 1. Ensure Test Isolation:

[TestInitialize]
public void Setup()
{
    // Fresh setup for each test
    this.mockRepository = new Mock<IRepository>();
}

  1. Avoid Static State:

    // ❌ BAD - Static state
    private static List<Entity> sharedEntities = new();
    
    // ✅ GOOD - Instance state
    private List<Entity> testEntities = new();
    

  2. Clean Up After Tests:

    [TestCleanup]
    public void Cleanup()
    {
        // Clean up resources
        this.component?.Dispose();
    }
    

Issue: Mock Setup Not Working

Symptoms: Mock returns default values instead of configured behavior.

Solutions: 1. Verify Mock Setup:

// Ensure setup is correct
mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
    .ReturnsAsync(new Entity());

  1. Check Parameter Matching:

    // ✅ GOOD - Matches any Guid
    mockRepository.Setup(r => r.GetByIdAsync(It.IsAny<Guid>()))
    
    // ✅ GOOD - Matches specific Guid
    mockRepository.Setup(r => r.GetByIdAsync(testId))
    

  2. Verify Mock.Object Usage:

    // ✅ GOOD - Use .Object property
    var processor = new Processor(mockRepository.Object);
    
    // ❌ BAD - Using mock directly
    var processor = new Processor(mockRepository);
    

Issue: Async Tests Not Completing

Symptoms: Async tests hang or timeout.

Solutions: 1. Use ConfigureAwait(false):

var result = await this.processor.ProcessAsync(input).ConfigureAwait(false);

  1. Avoid Deadlocks:

    // ❌ BAD - Can cause deadlock
    var result = this.processor.ProcessAsync(input).Result;
    
    // ✅ GOOD - Proper async/await
    var result = await this.processor.ProcessAsync(input);
    

  2. Use CancellationToken:

    var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token;
    var result = await this.processor.ProcessAsync(input, cancellationToken);
    

Summary

Unit testing in the ConnectSoft Microservice Template provides:

  • Fast Feedback: Tests execute in milliseconds
  • Test Isolation: Components tested without external dependencies
  • MSTest Framework: Standard .NET testing framework
  • Moq Integration: Mocking framework for test doubles
  • AAA Pattern: Clear Arrange-Act-Assert structure
  • Comprehensive Coverage: Tests for validators, processors, mappers, etc.
  • Best Practices: Patterns for maintainable, reliable tests

By following these patterns, teams can:

  • Maintain Quality: Catch bugs early in the development cycle
  • Enable Refactoring: Confidence to refactor with test safety net
  • Document Behavior: Tests serve as executable documentation
  • Improve Design: Testability encourages better code design
  • Speed Development: Fast feedback accelerates development cycles

Unit tests are the foundation of a robust testing strategy, providing fast, reliable validation of individual components before integration and deployment.