Testing Guide - Essert.MF¶
This guide explains the testing strategy, organization, and best practices for the Essert.MF project.
Table of Contents¶
- Testing Philosophy
- Test Organization
- Test Types
- Writing Tests
- Running Tests
- Best Practices
- Common Patterns
Testing Philosophy¶
The Essert.MF project follows hexagonal architecture principles, and the test suite is organized to reflect this architecture. Tests are separated by architectural layer and test type, ensuring:
- Fast Feedback: Domain and application tests run in milliseconds without external dependencies
- Isolation: Each layer tests its responsibilities in isolation using mocks/stubs
- Integration Confidence: Integration tests verify that adapters work with real external systems
- Performance Validation: Performance tests ensure the system meets non-functional requirements
Test Pyramid¶
┌─────────────┐
│ E2E/API │ ← Few, slow, expensive
│ (123) │ (Essert.MF.API.Rest.Tests)
└─────────────┘
┌─────────────────┐
│ Integration │ ← Some, slower, DB required
│ Tests │ (Infrastructure.Tests/Integration)
└─────────────────┘
┌───────────────────────┐
│ Unit Tests │ ← Many, fast, isolated
│ Domain + Application │ (Domain.Tests + Application.Tests)
│ (65+) │
└───────────────────────┘
Test Organization¶
By Architectural Layer¶
Tests are organized to match the hexagonal architecture layers:
Tests/
├── Essert.MF.Domain.Tests/ # Core business logic (pure unit tests)
├── Essert.MF.Application.Tests/ # Use cases (unit tests with mocks)
├── Essert.MF.Infrastructure.Tests/ # Infrastructure adapters (unit + integration)
└── Essert.MF.API.Rest.Tests/ # REST API adapter (integration tests)
Infrastructure Tests by Type¶
Infrastructure tests are further organized by test type:
Essert.MF.Infrastructure.Tests/
├── Unit/ # Fast, isolated, use mocks
│ ├── Repositories/ # Repository pattern logic
│ ├── Mapping/ # AutoMapper configurations
│ ├── Extensions/ # DI setup
│ └── Services/ # Infrastructure services
├── Integration/ # Slow, use real dependencies
│ ├── Persistence/ # Database operations
│ │ ├── DbContext/ # Database connectivity
│ │ ├── Operations/ # CRUD operations
│ │ └── Repositories/ # Repository implementations
│ └── Mapping/ # Complex mapping scenarios
└── Performance/ # Benchmarks and performance validation
├── Persistence/ # Database performance
├── Repository/ # Repository pattern performance
├── Service/ # Service layer performance
└── Tools/ # Performance measurement utilities
Test Types¶
1. Domain Tests (Pure Unit Tests)¶
Location: Essert.MF.Domain.Tests/
Purpose: Test business logic in isolation
Characteristics: - No external dependencies (no database, no file system, no network) - Fast execution (< 1 second for entire suite) - Test domain entities, value objects, and domain services - Use plain objects and assertions
Example:
public class ProcessStateTests
{
[Fact]
public void Active_State_Should_Allow_Transition_To_Completed()
{
// Arrange
var state = ProcessState.Active;
// Act
var canTransition = state.CanTransitionTo(ProcessState.Completed);
// Assert
canTransition.Should().BeTrue();
}
}
When to write: - Testing business rules - Testing value object validation - Testing entity behavior - Testing domain calculations
2. Application Tests (Use Case Tests)¶
Location: Essert.MF.Application.Tests/
Purpose: Test application use cases with mocked dependencies
Characteristics: - Use mocks for infrastructure dependencies (repositories, external services) - Fast execution (< 2 seconds for entire suite) - Test command/query handlers - Verify correct orchestration of domain logic
Example:
public class CreateProductHandlerTests
{
[Fact]
public async Task Handle_ValidCommand_CreatesProduct()
{
// Arrange
var mockRepo = new Mock<IProductRepository>();
var mockUnitOfWork = new Mock<IUnitOfWork>();
var handler = new CreateProductHandler(mockRepo.Object, mockUnitOfWork.Object);
var command = new CreateProductCommand("TestProduct", "1.0");
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
mockRepo.Verify(r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>()), Times.Once);
mockUnitOfWork.Verify(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
}
When to write: - Testing use case handlers - Testing application service logic - Testing validation rules - Testing error handling
3. Infrastructure Unit Tests¶
Location: Essert.MF.Infrastructure.Tests/Unit/
Purpose: Test infrastructure components in isolation
Characteristics: - Use in-memory databases or mocks - Fast execution - Test mapping configurations, DI setup, repository patterns
Example:
public class DomainToEfMappingProfileTests
{
[Fact]
public void Mapping_Configuration_Should_Be_Valid()
{
// Arrange
var config = new MapperConfiguration(cfg =>
cfg.AddProfile<DomainToEfMappingProfile>());
// Act & Assert
config.AssertConfigurationIsValid();
}
}
When to write: - Testing AutoMapper profiles - Testing DI configuration - Testing repository patterns without database - Testing infrastructure service logic
4. Infrastructure Integration Tests¶
Location: Essert.MF.Infrastructure.Tests/Integration/
Purpose: Test infrastructure adapters with real dependencies
Characteristics: - Use real databases (requires MySQL/MariaDB running) - Slower execution (10-30 seconds) - Test actual database operations, connectivity, relationships
Example:
public class WpcDbContextTests : IDisposable
{
private readonly WpcDbContext _context;
public WpcDbContextTests()
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.test.json")
.Build();
var connectionString = config.GetConnectionString("Wpc");
var options = new DbContextOptionsBuilder<WpcDbContext>()
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
.Options;
_context = new WpcDbContext(options);
}
[Fact]
public async Task WpcDbContext_CanConnect_To_Database()
{
// Act
var canConnect = await _context.Database.CanConnectAsync();
// Assert
canConnect.Should().BeTrue();
}
public void Dispose() => _context?.Dispose();
}
When to write: - Testing database connectivity - Testing CRUD operations with real database - Testing complex queries - Testing relationships and foreign keys - Testing transactions
5. Performance Tests¶
Location: Essert.MF.Infrastructure.Tests/Performance/
Purpose: Validate performance requirements
Characteristics: - Measure execution time, memory usage - Establish performance baselines - Use real database for realistic measurements
Example:
public class RepositoryPerformanceTests
{
[Fact]
public async Task GetAll_Products_Should_Complete_Within_Threshold()
{
// Arrange
var stopwatch = Stopwatch.StartNew();
var repository = CreateProductRepository();
// Act
var products = await repository.GetAllAsync();
stopwatch.Stop();
// Assert
products.Should().NotBeEmpty();
stopwatch.ElapsedMilliseconds.Should().BeLessThan(5000); // 5 second threshold
}
}
When to write: - Establishing performance baselines - Verifying query optimization - Testing bulk operations - Measuring memory usage
6. API Integration Tests¶
Location: Essert.MF.API.Rest.Tests/
Purpose: Test REST API endpoints end-to-end
Characteristics: - Use TestServer or WebApplicationFactory - Test HTTP requests/responses - Verify serialization, status codes, error handling - May use real or in-memory database
Example:
public class ProductsControllerTests : IClassFixture<TestWebApplicationFactory>
{
private readonly HttpClient _client;
public ProductsControllerTests(TestWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProducts_ReturnsOkResponse()
{
// Act
var response = await _client.GetAsync("/api/products");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().NotBeNullOrEmpty();
}
}
When to write: - Testing API contracts - Testing HTTP status codes - Testing request/response serialization - Testing authentication/authorization - Testing error responses
Running Tests¶
By Speed (Recommended for Development)¶
# Fast tests only (< 3 seconds) - Run on every save
dotnet test Essert.MF.Domain.Tests && \
dotnet test Essert.MF.Application.Tests && \
dotnet test --filter "FullyQualifiedName~Infrastructure.Tests.Unit"
# With integration tests (~30 seconds) - Run before commit
dotnet test --filter "FullyQualifiedName~Integration"
# Full suite (~2 minutes) - Run before push
dotnet test
By Architectural Layer¶
# Domain Layer
dotnet test Essert.MF.Domain.Tests
# Application Layer
dotnet test Essert.MF.Application.Tests
# Infrastructure Layer
dotnet test Essert.MF.Infrastructure.Tests
# API Layer
dotnet test Essert.MF.API.Rest.Tests
By Test Type¶
# Unit tests only (fast)
dotnet test --filter "FullyQualifiedName~Unit"
# Integration tests only (slow, requires DB)
dotnet test --filter "FullyQualifiedName~Integration"
# Performance tests only
dotnet test --filter "FullyQualifiedName~Performance"
By Feature Area¶
# Manufacturing domain tests
dotnet test --filter "FullyQualifiedName~Manufacturing"
# Product domain tests
dotnet test --filter "FullyQualifiedName~Product"
# WPC-specific tests
dotnet test --filter "FullyQualifiedName~Wpc"
Best Practices¶
General Principles¶
- Test Behavior, Not Implementation
- Focus on what the code does, not how it does it
- Avoid testing private methods directly
-
Test through public interfaces
-
Follow AAA Pattern
-
One Assertion Per Test (when reasonable)
- Each test should verify one specific behavior
- Related assertions can be grouped
-
Use
FluentAssertionsfor expressive assertions -
Test Names Should Be Descriptive
Domain Layer Tests¶
- No Mocks Needed
- Domain tests should use real objects
-
No infrastructure dependencies to mock
-
Test Business Rules Explicitly
[Theory] [InlineData(ProcessState.Active, ProcessState.Completed, true)] [InlineData(ProcessState.Completed, ProcessState.Active, false)] public void State_Transitions_Should_Follow_Business_Rules( ProcessState from, ProcessState to, bool expected) { var canTransition = from.CanTransitionTo(to); canTransition.Should().Be(expected); } -
Test Value Object Validation
Application Layer Tests¶
-
Always Mock Infrastructure Dependencies
-
Verify Repository Interactions
-
Test Error Handling
[Fact] public async Task Handle_RepositoryThrowsException_PropagatesException() { // Arrange _mockRepository .Setup(r => r.AddAsync(It.IsAny<Product>(), It.IsAny<CancellationToken>())) .ThrowsAsync(new InvalidOperationException("Database error")); var handler = new CreateProductHandler(_mockRepository.Object, _mockUnitOfWork.Object); // Act Func<Task> act = async () => await handler.Handle(command, CancellationToken.None); // Assert await act.Should().ThrowAsync<InvalidOperationException>(); }
Infrastructure Layer Tests¶
Unit Tests¶
-
Use In-Memory Databases When Possible
-
Test Mapping Configurations
Integration Tests¶
-
Use Real Database Connections
-
Clean Up After Tests
-
Use Test Transactions (Rollback After Test)
API Tests¶
-
Use TestWebApplicationFactory
-
Test HTTP Status Codes
-
Test Response Content
Common Patterns¶
Test Data Builders¶
Use builder pattern for complex test data:
public class ProductBuilder
{
private string _name = "Default Product";
private VersionNumber _version = VersionNumber.Create("1.0");
public ProductBuilder WithName(string name)
{
_name = name;
return this;
}
public ProductBuilder WithVersion(string version)
{
_version = VersionNumber.Create(version);
return this;
}
public Product Build()
{
return Product.Create(_name, _version);
}
}
// Usage
var product = new ProductBuilder()
.WithName("CustomProduct")
.WithVersion("2.0")
.Build();
Test Fixtures (xUnit)¶
Share expensive setup across tests:
public class DatabaseFixture : IDisposable
{
public DbContext Context { get; }
public DatabaseFixture()
{
// Expensive setup
Context = CreateDatabaseContext();
}
public void Dispose()
{
Context?.Dispose();
}
}
public class MyTests : IClassFixture<DatabaseFixture>
{
private readonly DbContext _context;
public MyTests(DatabaseFixture fixture)
{
_context = fixture.Context;
}
}
Theory Tests for Multiple Scenarios¶
Use [Theory] for testing multiple inputs:
[Theory]
[InlineData("v1.0", true)]
[InlineData("v2.0.1", true)]
[InlineData("invalid", false)]
[InlineData("", false)]
public void VersionNumber_Validation(string input, bool isValid)
{
// Act
var result = VersionNumber.TryCreate(input, out var version);
// Assert
result.Should().Be(isValid);
}
Custom Assertions¶
Create custom assertions for domain-specific validation:
public static class ProductAssertions
{
public static void ShouldBeValidProduct(this Product product)
{
product.Should().NotBeNull();
product.Name.Should().NotBeNullOrEmpty();
product.Version.Should().NotBeNull();
product.Parameters.Should().NotBeEmpty();
}
}
// Usage
product.ShouldBeValidProduct();
CI/CD Integration¶
Recommended Test Execution Strategy¶
-
On Every Commit (< 5 seconds)
-
On Pull Request (< 30 seconds)
-
Nightly Build (full suite, ~2 minutes)
Test Coverage¶
Aim for: - Domain Layer: 90%+ coverage (critical business logic) - Application Layer: 80%+ coverage (use cases) - Infrastructure Layer: 70%+ coverage (focus on critical paths) - API Layer: 80%+ coverage (contracts)
Check coverage with:
Troubleshooting¶
Common Issues¶
- Tests fail with "Database connection error"
- Ensure MySQL/MariaDB is running on localhost
- Check connection strings in
appsettings.test.json -
Verify database user has appropriate permissions
-
Tests are slow
- Ensure you're running unit tests, not integration tests
- Check for unnecessary database operations
-
Use
--filterto run specific test categories -
AutoMapper configuration errors
- Run
DomainToEfMappingProfileTestsfirst - Verify all mappings are registered
-
Check for circular dependencies
-
Flaky tests
- Avoid dependencies on external state
- Use test transactions that rollback
- Avoid time-based assertions (use timeouts instead)
Additional Resources¶
- xUnit Documentation
- FluentAssertions Documentation
- Moq Documentation
- Microsoft Testing Best Practices
Summary¶
- Organize tests by architectural layer
- Write fast unit tests for domain and application layers
- Use integration tests for infrastructure adapters
- Follow AAA pattern and write descriptive test names
- Run fast tests frequently, integration tests before commits
- Aim for high coverage on business-critical code