Architecture Tests in ConnectSoft Microservice Template¶
Purpose & Overview¶
Architecture Tests are automated tests that validate architectural rules and constraints at build time, ensuring that the codebase maintains its intended structure and prevents architectural violations. In the ConnectSoft Microservice Template, architecture tests use NetArchTest.Rules to enforce Clean Architecture principles, dependency rules, naming conventions, and design patterns.
Why Architecture Tests?¶
Architecture tests provide several critical benefits:
- Build-Time Enforcement: Architectural violations fail builds automatically
- Living Documentation: Tests serve as executable documentation of architectural rules
- Prevent Architectural Drift: Catch violations before they spread through the codebase
- Team Alignment: Shared understanding of architectural constraints
- Refactoring Safety: Confidence that refactoring won't break architectural rules
- CI/CD Integration: Automated validation in every build
- Code Review Support: Objective, measurable architecture validation
Architecture Tests Philosophy
Architecture tests transform architectural rules from documentation into executable code. They fail fast, provide clear feedback, and prevent architectural degradation over time. Unlike manual code reviews, architecture tests are objective, repeatable, and catch violations before they become problems.
Architecture Overview¶
Architecture Tests in the Template¶
The ConnectSoft Microservice Template includes comprehensive architecture tests organized by concern:
ArchitectureTests Project
├── EnforcingLayeredArchitectureUnitTests.cs
│ ├── ControllersShouldNotDirectlyReferencePersistenceModel
│ ├── ControllersShouldNotDependOnAnyDomainModelClassServiceImplementation
│ ├── GrpcServicesShouldNotDirectlyReferencePersistenceModel
│ ├── GrpcServicesShouldNotDependOnAnyDomainModelClassServiceImplementation
│ ├── DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations
│ ├── LowerLayersShouldNotReferenceUpperLayers
│ ├── ApplicationModelShouldNotReferenceConcretePersistenceImplementations
│ └── EntityModelShouldNotReferenceInfrastructureLayers
├── OptionsArchitectureUnitTests.cs
│ ├── OptionsShouldBeImplementedAsClasses
│ ├── OptionsShouldBeSealedClasses
│ ├── OptionsClassesShouldBePublic
│ └── OptionsClassesShouldHavePublicProperties
├── NamingConventionsArchitectureUnitTests.cs
│ ├── InterfacesShouldStartWithIPrefix
│ ├── RepositoryInterfacesShouldEndWithRepository
│ ├── UseCaseInterfacesShouldEndWithUseCase
│ ├── InputClassesShouldEndWithInput
│ ├── OutputClassesShouldEndWithOutput
│ ├── CommandsShouldEndWithCommand
│ ├── EventsShouldEndWithEvent
│ ├── OptionsClassesShouldEndWithOptions
│ ├── ExtensionMethodContainersShouldEndWithExtensions
│ └── ExceptionsShouldEndWithException
├── DomainEventPatternArchitectureUnitTests.cs
│ ├── DomainEventsShouldBeInDomainLayer
│ └── DomainEventsShouldNotBeInInfrastructureLayers
├── DomainServicePatternArchitectureUnitTests.cs
│ ├── DomainServiceInterfacesShouldImplementIDomainService
│ ├── DomainServiceImplementationsShouldBeInDomainModelImpl
│ ├── DomainServicesShouldNotDependOnInfrastructure
│ └── DomainServiceInterfacesShouldBeInDomainModel
├── UseCasePatternArchitectureUnitTests.cs
│ ├── UseCaseInterfacesShouldImplementIUseCase
│ ├── UseCaseInterfacesShouldEndWithUseCase
│ ├── UseCaseInterfacesShouldBeInDomainModel
│ ├── UseCaseImplementationsShouldBeInDomainModelImpl
│ └── UseCasesShouldHaveCorrespondingInputOutputClasses
├── RepositoryPatternArchitectureUnitTests.cs
│ ├── RepositoryInterfacesShouldInheritFromIGenericRepository
│ ├── RepositoryImplementationsShouldBeInImplementationProjects
│ ├── RepositoryImplementationsShouldImplementInterfaces
│ └── DomainModelShouldNotReferenceRepositoryImplementations
├── MessagingModelArchitectureUnitTests.cs
│ ├── CommandsShouldImplementICommand
│ ├── EventsShouldImplementIEvent
│ ├── CommandsShouldEndWithCommand
│ ├── EventsShouldEndWithEvent
│ ├── MessagesShouldBeInMessagingModelOnly
│ └── MessagesShouldNotHaveInfrastructureDependencies
├── CrossCuttingConcernsArchitectureUnitTests.cs
│ ├── ExceptionsShouldEndWithException
│ ├── ExtensionMethodsShouldBeInternalAndStatic
│ ├── OptionsClassesShouldUseDataAnnotationsForValidation
│ └── ConstantsShouldBeInConstantClasses
├── EntityModelIsolationArchitectureUnitTests.cs
│ ├── EntityModelShouldNotReferencePersistenceImplementations
│ ├── EntityModelShouldNotReferenceMessagingModels
│ ├── EntityModelShouldNotReferenceServiceModels
│ ├── EntityInterfacesShouldImplementIGenericEntity
│ └── AggregateRootInterfacesShouldImplementIGenericAggregateRoot
├── ApplicationModelIsolationArchitectureUnitTests.cs
│ ├── ApplicationModelShouldNotReferenceConcretePersistenceImplementations
│ └── ExtensionMethodClassesShouldBeInternalAndStatic
└── SpecificationPatternArchitectureUnitTests.cs
├── SpecificationInterfacesShouldImplementISpecification
├── SpecificationsShouldEndWithSpecification
└── SpecificationsShouldBeInPersistenceModel
Test Categories¶
All architecture tests are tagged with [TestCategory("Architecture Unit Tests")] for easy filtering:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
// Test implementation
}
NetArchTest.Rules¶
What is NetArchTest.Rules?¶
NetArchTest.Rules is a fluent testing library for .NET that validates architectural rules using reflection. It provides a fluent API for querying assemblies, types, and dependencies.
Key Features¶
- Fluent API: Expressive, readable test syntax
- Type-Based Queries: Query assemblies, namespaces, and types
- Dependency Analysis: Validate dependency relationships
- Pattern Matching: Match types by name, namespace, inheritance, interfaces
- Performance: Fast execution using reflection
Installation¶
Basic Usage Pattern¶
using NetArchTest.Rules;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestMethod]
public void MyArchitectureRule()
{
// Arrange: Define types to test
var types = Types.InAssembly(typeof(MyClass).Assembly)
.That()
.ResideInNamespace("My.Namespace")
.And()
.AreClasses();
// Act: Apply rules
var result = types
.Should()
.NotHaveDependencyOn("Forbidden.Assembly")
.GetResult();
// Assert: Validate results
Assert.IsTrue(result.IsSuccessful, result.FailingTypes?.ToString());
}
Architecture Test Patterns¶
Dependency Rule Tests¶
Pattern: Validate that certain types don't depend on forbidden assemblies.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
// Arrange
Type controllersType = typeof(MicroserviceAggregateRootsServiceController);
List<string> repositoriesDependencies = new();
string? repositoriesAssemblyName = typeof(IMicroserviceAggregateRootsRepository).Assembly.FullName;
if (!string.IsNullOrEmpty(repositoriesAssemblyName))
{
repositoriesDependencies.Add(repositoriesAssemblyName);
}
#if UseMongoDb
string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
{
repositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
}
#endif
#if UseNHibernate
string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
{
repositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
}
#endif
// Act
bool result = Types.InAssembly(controllersType.Assembly)
.That()
.ResideInNamespace(controllersType.Namespace)
.ShouldNot()
.HaveDependencyOnAny(repositoriesDependencies.ToArray())
.GetResult()
.IsSuccessful;
// Assert
Assert.IsTrue(result);
}
Key Techniques:
- Conditional Compilation: Use #if directives for optional features
- Assembly References: Use typeof() to get assembly information
- Multiple Dependencies: Check against multiple forbidden dependencies
- Namespace Filtering: Limit scope to specific namespaces
Implementation Dependency Tests¶
Pattern: Validate that types depend on interfaces, not implementations.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDependOnAnyDomainModelClassServiceImplementation()
{
// Arrange
PredicateList webApiControllers =
Types.InAssembly(typeof(MicroserviceAggregateRootsServiceController).Assembly)
.That()
.AreClasses()
.And().ArePublic()
.And().Inherit(typeof(ControllerBase));
// Get all domain model implementation classes
string?[] services = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
.That()
.ArePublic()
.And().AreClasses()
.GetTypes()
.Where(localType => !string.IsNullOrWhiteSpace(localType.FullName))
.Select(localType => localType.FullName)
.Distinct(StringComparer.Ordinal)
.ToArray();
// Act
NetArchTest.Rules.TestResult testResult = webApiControllers
.Should()
.NotHaveDependencyOnAny(services)
.GetResult();
// Assert
Assert.IsTrue(
testResult.IsSuccessful,
"Web API controllers must * not * depend on any domain model service implementation");
}
Key Techniques:
- Type Queries: Use GetTypes() to get all types matching criteria
- LINQ Filtering: Use LINQ to filter and transform types
- PredicateList: Use PredicateList for complex queries
- Clear Error Messages: Include descriptive assertion messages
Layer Dependency Tests¶
Pattern: Validate unidirectional dependency flow (outer → inner).
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void LowerLayersShouldNotReferenceUpperLayers()
{
// Domain model should not reference service model
bool result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
// Persistence model should not reference service model
result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
// Persistence model should not reference domain model
result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.DomainModel")
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
}
Key Techniques: - Multiple Assertions: Test multiple layer relationships in one test - Namespace Matching: Use string-based namespace matching - Assembly-Based: Start from assembly to ensure comprehensive coverage
Contract vs Implementation Tests¶
Pattern: Validate that Application Layer uses interfaces, not concrete implementations.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations()
{
Type domainModelImplType = typeof(DefaultMicroserviceAggregateRootsRetriever);
List<string> concreteRepositoriesDependencies = new();
#if UseMongoDb
string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
{
concreteRepositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
}
#endif
#if UseNHibernate
string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
{
concreteRepositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
}
#endif
// Assert: Should NOT depend on concrete implementations
bool result = Types.InAssembly(domainModelImplType.Assembly)
.That()
.ResideInNamespace(domainModelImplType.Namespace)
.ShouldNot()
.HaveDependencyOnAny(concreteRepositoriesDependencies.ToArray())
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
// Assert: SHOULD depend on interfaces
result = Types.InAssembly(domainModelImplType.Assembly)
.That()
.HaveName(
nameof(DefaultMicroserviceAggregateRootsRetriever),
nameof(DefaultMicroserviceAggregateRootsProcessor))
.And()
.ResideInNamespace(domainModelImplType.Namespace)
.Should()
.HaveDependencyOn(typeof(IMicroserviceAggregateRootsRepository).AssemblyQualifiedName)
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
}
Key Techniques:
- Positive and Negative Assertions: Test both what should and shouldn't exist
- Type Name Filtering: Use HaveName() for specific types
- Multiple Conditions: Combine multiple conditions with And()
Naming Convention Tests¶
Pattern: Validate naming conventions and type characteristics across the solution.
Interface Naming:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void InterfacesShouldStartWithIPrefix()
{
var interfaces = Types.InCurrentDomain()
.That()
.AreInterfaces()
.GetTypes()
.Where(type =>
{
var ns = type.Namespace ?? string.Empty;
return ns.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal);
})
.ToList();
var failingTypes = interfaces
.Where(i => !i.Name.StartsWith("I", StringComparison.Ordinal))
.ToList();
Assert.IsTrue(
failingTypes.Count == 0,
$"Interfaces not starting with 'I': {string.Join(", ", failingTypes.Select(t => t.FullName))}");
}
Options Naming:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void OptionsShouldBeImplementedAsClasses()
{
Assembly optionsAssembly = typeof(MicroserviceOptions).Assembly;
var result = Types.InAssembly(optionsAssembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.Options")
.And()
.HaveNameEndingWith("Options", System.StringComparison.Ordinal)
.Should()
.BeClasses()
.GetResult()
.IsSuccessful;
Assert.IsTrue(result);
}
Key Techniques:
- Name Pattern Matching: Use HaveNameEndingWith(), HaveNameMatching() for naming conventions
- Type Characteristics: Use BeClasses(), BeSealed(), etc.
- Namespace Filtering: Limit scope to specific namespaces
- Custom Filtering: Use LINQ for complex filtering logic
Sealed Class Tests¶
Pattern: Validate that certain types are sealed (with exceptions).
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void OptionsShouldBeSealedClasses()
{
Assembly optionsAssembly = typeof(MicroserviceOptions).Assembly;
var result = Types.InAssembly(optionsAssembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.Options")
.And()
.HaveNameEndingWith("Options", System.StringComparison.Ordinal)
.And()
.DoNotHaveName("DnsCommonOptions")
.And()
.DoNotHaveName("MassTransitReceiveEndpointOptions")
.And()
.DoNotImplementInterface(typeof(IValidateOptions<>))
.Should()
.BeSealed()
.GetResult();
Assert.IsTrue(result.IsSuccessful);
}
Key Techniques:
- Exception Handling: Use DoNotHaveName() for exceptions
- Interface Exclusion: Use DoNotImplementInterface() to exclude validators
- Multiple Exclusions: Chain multiple exclusion conditions
NetArchTest.Rules API Reference¶
Querying Types¶
Selecting Types:
// All types in an assembly
Types.InAssembly(typeof(MyClass).Assembly)
// Types in a namespace
Types.InAssembly(assembly).That().ResideInNamespace("My.Namespace")
// Types by name pattern
Types.InAssembly(assembly).That().HaveNameEndingWith("Repository")
// Types by inheritance
Types.InAssembly(assembly).That().Inherit(typeof(MyBaseClass))
// Types by interface
Types.InAssembly(assembly).That().ImplementInterface(typeof(IMyInterface))
Filtering Conditions:
// Combine conditions
.That()
.AreClasses()
.And().ArePublic()
.And().ResideInNamespace("My.Namespace")
// Exclude conditions
.That()
.HaveNameEndingWith("Options")
.And().DoNotHaveName("ExceptionOptions")
.And().DoNotImplementInterface(typeof(IValidator))
Type Characteristics:
.AreClasses() // Must be classes
.AreInterfaces() // Must be interfaces
.AreAbstract() // Must be abstract
.AreSealed() // Must be sealed
.ArePublic() // Must be public
.AreNotPublic() // Must not be public
Dependency Rules¶
Checking Dependencies:
// Should not depend on
.ShouldNot()
.HaveDependencyOn("Forbidden.Assembly")
// Should not depend on any
.ShouldNot()
.HaveDependencyOnAny("Assembly1", "Assembly2")
// Should depend on
.Should()
.HaveDependencyOn("Required.Assembly")
// Should depend on any
.Should()
.HaveDependencyOnAny("Assembly1", "Assembly2")
Dependency Matching:
// By assembly name
.HaveDependencyOn("MyAssembly")
// By fully qualified type name
.HaveDependencyOn(typeof(MyClass).AssemblyQualifiedName)
// By namespace
.HaveDependencyOn("My.Namespace")
Getting Results¶
Executing Queries:
// Get result
var result = types
.Should()
.NotHaveDependencyOn("Forbidden.Assembly")
.GetResult();
// Check success
bool isSuccessful = result.IsSuccessful;
// Get failing types
var failingTypes = result.FailingTypes;
// Get passing types
var passingTypes = result.PassingTypes;
Result Information:
public class TestResult
{
public bool IsSuccessful { get; }
public IEnumerable<Type> FailingTypes { get; }
public IEnumerable<Type> PassingTypes { get; }
public string ErrorMessage { get; }
}
Writing Effective Architecture Tests¶
Test Structure¶
Follow AAA Pattern:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void YourArchitectureRule()
{
// Arrange: Set up test data and queries
var typesToTest = Types.InAssembly(typeof(MyClass).Assembly)
.That()
.ResideInNamespace("My.Namespace");
// Act: Execute the rule
var result = typesToTest
.Should()
.NotHaveDependencyOn("Forbidden.Assembly")
.GetResult();
// Assert: Validate the result
Assert.IsTrue(
result.IsSuccessful,
$"The following types violate the rule: {string.Join(", ", result.FailingTypes)}");
}
Descriptive Test Names¶
Use Descriptive Names:
// ✅ GOOD - Clear and descriptive
public void ControllersShouldNotDirectlyReferencePersistenceModel()
public void DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations()
public void OptionsShouldBeSealedClasses()
// ❌ BAD - Vague
public void TestDependencies()
public void ValidateOptions()
Comprehensive Error Messages¶
Include Failing Types in Messages:
// ✅ GOOD - Includes failing types
Assert.IsTrue(
result.IsSuccessful,
$"The following types violate the rule: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
// ❌ BAD - Generic message
Assert.IsTrue(result.IsSuccessful);
Conditional Compilation¶
Handle Optional Features:
#if UseMongoDb
string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
{
repositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
}
#endif
#if UseNHibernate
string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
{
repositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
}
#endif
Group Related Tests¶
Organize by Concern:
[TestClass]
public class EnforcingLayeredArchitectureUnitTests
{
// All layered architecture tests
}
[TestClass]
public class OptionsArchitectureUnitTests
{
// All options-related tests
}
Running Architecture Tests¶
Local Execution¶
Run All Architecture Tests:
Run Specific Test Class:
Run Specific Test:
Verbose Output:
CI/CD Integration¶
Azure DevOps Pipeline:
- task: DotNetCoreCLI@2
displayName: 'Run Architecture Tests'
inputs:
command: 'test'
projects: '**/*ArchitectureTests.csproj'
arguments: '--filter "TestCategory=Architecture Unit Tests" --logger trx --results-directory $(Agent.TempDirectory)'
GitHub Actions:
- name: Run Architecture Tests
run: |
dotnet test --filter "TestCategory=Architecture Unit Tests" --logger "trx;LogFileName=architecture-tests.trx"
Benefits: - Build-Time Validation: Fail builds on violations - Early Detection: Catch issues before code review - Continuous Enforcement: Validate on every commit - Test Reports: Generate test result files for analysis
Debugging Failed Tests¶
Understanding Test Failures¶
Common Failure Scenarios:
- False Positives: Test is too strict
- Legitimate Violations: Code violates architectural rule
- Missing Dependencies: Test doesn't account for all cases
- Conditional Compilation: Test doesn't handle
#ifdirectives
Debugging Techniques¶
1. Inspect Failing Types:
var result = types
.Should()
.NotHaveDependencyOn("Forbidden.Assembly")
.GetResult();
if (!result.IsSuccessful)
{
foreach (var failingType in result.FailingTypes)
{
Console.WriteLine($"Failing type: {failingType.FullName}");
// Inspect dependencies
var dependencies = failingType.GetReferencedAssemblies();
foreach (var dep in dependencies)
{
Console.WriteLine($" Depends on: {dep.Name}");
}
}
}
2. Use Detailed Output:
3. Add Debugging Code:
var typesToTest = Types.InAssembly(typeof(MyClass).Assembly)
.That()
.ResideInNamespace("My.Namespace")
.GetTypes(); // Get all matching types first
Console.WriteLine($"Found {typesToTest.Count()} types to test");
foreach (var type in typesToTest)
{
Console.WriteLine($" - {type.FullName}");
}
Fixing Violations¶
Example: Controller Violation:
// ❌ BAD - Violates architecture rule
public class MyController : ControllerBase
{
private readonly IMicroserviceAggregateRootsRepository _repository; // Violation!
public MyController(IMicroserviceAggregateRootsRepository repository)
{
_repository = repository;
}
}
// ✅ GOOD - Complies with architecture rule
public class MyController : ControllerBase
{
private readonly IMicroserviceAggregateRootsRetriever _retriever; // Correct!
private readonly IMicroserviceAggregateRootsProcessor _processor; // Correct!
public MyController(
IMicroserviceAggregateRootsRetriever retriever,
IMicroserviceAggregateRootsProcessor processor)
{
_retriever = retriever;
_processor = processor;
}
}
Additional Architecture Test Patterns¶
Naming Conventions Tests¶
Pattern: Validate consistent naming conventions across the solution.
Repository Naming:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryInterfacesShouldEndWithRepository()
{
var result = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
.Should()
.HaveNameEndingWith("Repository", StringComparison.Ordinal)
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Repository interfaces not ending with 'Repository': {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Use Case Naming:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCaseInterfacesShouldEndWithUseCase()
{
var result = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
.And()
.ImplementInterface(typeof(IUseCase<,>))
.Should()
.HaveNameEndingWith("UseCase", StringComparison.Ordinal)
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Use case interfaces must end with 'UseCase': {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Domain Event Pattern Tests¶
Pattern: Validate domain events are in correct layers and don't depend on infrastructure.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainEventsShouldBeInDomainLayer()
{
var domainEvents = Types.InCurrentDomain()
.That()
.AreClasses()
.And()
.HaveNameEndingWith("Event", StringComparison.Ordinal)
.GetTypes()
.Where(type =>
{
var ns = type.Namespace ?? string.Empty;
return ns.StartsWith("ConnectSoft.MicroserviceTemplate.EntityModel", StringComparison.Ordinal) ||
ns.StartsWith("ConnectSoft.MicroserviceTemplate.DomainModel", StringComparison.Ordinal);
})
.ToList();
Assert.IsGreaterThanOrEqualTo(0, domainEvents.Count,
$"Found {domainEvents.Count} domain events in EntityModel or DomainModel");
}
Domain Service Pattern Tests¶
Pattern: Validate domain services implement interfaces and follow isolation rules.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainServiceInterfacesShouldImplementIDomainService()
{
var result = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
.Should()
.ImplementInterface(typeof(IDomainService))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Domain service interfaces must implement IDomainService: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Domain Service Isolation:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainServicesShouldNotDependOnInfrastructure()
{
var domainModelImplAssembly = typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly;
#if UseMongoDb
var mongoDbResult = Types.InAssembly(domainModelImplAssembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel.Impl")
.And()
.ImplementInterface(typeof(IDomainService))
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb")
.GetResult();
Assert.IsTrue(mongoDbResult.IsSuccessful,
$"Domain services should not depend on MongoDB: {string.Join(", ", mongoDbResult.FailingTypes.Select(t => t.FullName))}");
#endif
}
Use Case Pattern Tests¶
Pattern: Validate use cases follow the pattern with Input/Output types.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCasesShouldHaveCorrespondingInputOutputClasses()
{
var useCaseInterfaces = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ImplementInterface(typeof(IUseCase<,>))
.GetTypes();
foreach (var useCaseInterface in useCaseInterfaces)
{
var genericArgs = useCaseInterface.GetGenericArguments();
if (genericArgs.Length == 2)
{
var inputType = genericArgs[0];
var outputType = genericArgs[1];
if (!inputType.Name.EndsWith("Input", StringComparison.Ordinal))
{
Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that does not end with 'Input'");
}
if (!outputType.Name.EndsWith("Output", StringComparison.Ordinal))
{
Assert.Fail($"Use case {useCaseInterface.Name} has output type {outputType.Name} that does not end with 'Output'");
}
}
}
}
Repository Pattern Tests¶
Pattern: Validate repository interfaces and implementations follow the pattern.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryInterfacesShouldInheritFromIGenericRepository()
{
var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
!a.GetName().Name.Contains("Test"))
.ToList();
var result = Types.InAssemblies(allAssemblies)
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
.And()
.HaveNameEndingWith("Repository", StringComparison.Ordinal)
.Should()
.ImplementInterface(typeof(IGenericRepository<,>))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Repository interfaces must inherit from IGenericRepository: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Repository Implementation Validation:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryImplementationsShouldImplementInterfaces()
{
var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
!a.GetName().Name.Contains("Test"))
.ToList();
var repositoryInterfaces = Types.InAssemblies(allAssemblies)
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
.And()
.HaveNameEndingWith("Repository", StringComparison.Ordinal)
.GetTypes()
.ToList();
#if UseMongoDb
var mongoDbRepositories = Types.InAssemblies(allAssemblies)
.That()
.AreClasses()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb.Repositories")
.And()
.HaveNameEndingWith("Repository", StringComparison.Ordinal)
.GetTypes();
foreach (var repository in mongoDbRepositories)
{
var implementsRepositoryInterface = repositoryInterfaces.Any(iface =>
iface.IsAssignableFrom(repository));
if (!implementsRepositoryInterface)
{
Assert.Fail($"MongoDB repository {repository.Name} should implement a repository interface");
}
}
#endif
}
Messaging Model Tests¶
Pattern: Validate messaging model (commands and events) follow conventions.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void CommandsShouldImplementICommand()
{
var result = Types.InCurrentDomain()
.That()
.AreClasses()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.MessagingModel")
.And()
.HaveNameEndingWith("Command", StringComparison.Ordinal)
.Should()
.ImplementInterface(typeof(ICommand))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Command classes must implement ICommand: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Message Isolation:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void MessagesShouldNotHaveInfrastructureDependencies()
{
var serviceModelResult = Types.InCurrentDomain()
.That()
.AreClasses()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.MessagingModel")
.And()
.ImplementInterface(typeof(ICommand))
.Or()
.ImplementInterface(typeof(IEvent))
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
.GetResult();
Assert.IsTrue(serviceModelResult.IsSuccessful,
$"Messages should not depend on ServiceModel: {string.Join(", ", serviceModelResult.FailingTypes.Select(t => t.FullName))}");
}
Entity Model Isolation Tests¶
Pattern: Validate entity model isolation from infrastructure.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void EntityInterfacesShouldImplementIGenericEntity()
{
var result = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.EntityModel")
.And()
.HaveNameEndingWith("Entity", StringComparison.Ordinal)
.Should()
.ImplementInterface(typeof(IGenericEntity<>))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Entity interfaces should implement IGenericEntity: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Specification Pattern Tests¶
Pattern: Validate specification pattern implementation.
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void SpecificationInterfacesShouldImplementISpecification()
{
var result = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Specifications")
.Should()
.ImplementInterface(typeof(ISpecification<,>))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Specification interfaces must implement ISpecification: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}
Cross-Cutting Concerns Tests¶
Pattern: Validate cross-cutting concerns follow conventions.
Extension Methods:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ExtensionMethodsShouldBeInternalAndStatic()
{
var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
!a.GetName().Name.Contains("Test"))
.ToList();
var extensionClasses = Types.InAssemblies(allAssemblies)
.That()
.AreClasses()
.And()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.ApplicationModel")
.And()
.HaveNameEndingWith("Extensions", StringComparison.Ordinal)
.GetTypes();
foreach (var extensionClass in extensionClasses)
{
var methods = extensionClass.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
.Where(m => m.IsStatic &&
m.GetParameters().Length > 0 &&
m.GetParameters()[0].GetCustomAttribute<ExtensionAttribute>() != null);
foreach (var method in methods)
{
if (method.IsPublic)
{
Assert.Fail($"Extension method {method.Name} in {extensionClass.Name} should be internal, not public");
}
}
}
}
Advanced Patterns¶
Testing Multiple Rules¶
Combine Multiple Rules in One Test:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void LowerLayersShouldNotReferenceUpperLayers()
{
// Test multiple relationships
var results = new List<bool>
{
// Domain model should not reference service model
Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
.GetResult()
.IsSuccessful,
// Persistence model should not reference service model
Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
.ShouldNot()
.HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
.GetResult()
.IsSuccessful
};
Assert.IsTrue(results.All(r => r), "One or more layer dependency rules violated");
}
Testing Interface Implementation¶
Validate Interface Implementations:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoriesShouldImplementIRepository()
{
var result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
.That()
.HaveNameEndingWith("Repository")
.And()
.AreClasses()
.Should()
.ImplementInterface(typeof(IMicroserviceAggregateRootsRepository))
.GetResult();
Assert.IsTrue(result.IsSuccessful,
$"Repositories must implement IMicroserviceAggregateRootsRepository: {string.Join(", ", result.FailingTypes)}");
}
Testing Naming Conventions¶
Validate Naming Patterns:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ProcessorsShouldEndWithProcessor()
{
var result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsProcessor).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel.Impl")
.And()
.ImplementInterface(typeof(IMicroserviceAggregateRootsProcessor))
.Should()
.HaveNameEndingWith("Processor")
.GetResult();
Assert.IsTrue(result.IsSuccessful);
}
Testing with Multiple Assemblies¶
Scan Across Solution:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void InterfacesShouldStartWithIPrefix()
{
// Get all ConnectSoft.MicroserviceTemplate assemblies
var interfaces = Types.InCurrentDomain()
.That()
.AreInterfaces()
.GetTypes()
.Where(type =>
{
var ns = type.Namespace ?? string.Empty;
return ns.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal);
})
.ToList();
var failingTypes = interfaces
.Where(i => !i.Name.StartsWith("I", StringComparison.Ordinal))
.ToList();
Assert.IsTrue(
failingTypes.Count == 0,
$"Interfaces not starting with 'I': {string.Join(", ", failingTypes.Select(t => t.FullName))}");
}
Testing Generic Type Arguments¶
Validate Generic Type Constraints:
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCasesShouldHaveCorrespondingInputOutputClasses()
{
var useCaseInterfaces = Types.InCurrentDomain()
.That()
.AreInterfaces()
.And()
.ImplementInterface(typeof(IUseCase<,>))
.GetTypes();
foreach (var useCaseInterface in useCaseInterfaces)
{
var genericArgs = useCaseInterface.GetGenericArguments();
if (genericArgs.Length == 2)
{
var inputType = genericArgs[0];
var outputType = genericArgs[1];
// Validate naming conventions
if (!inputType.Name.EndsWith("Input", StringComparison.Ordinal))
{
Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that does not end with 'Input'");
}
if (!outputType.Name.EndsWith("Output", StringComparison.Ordinal))
{
Assert.Fail($"Use case {useCaseInterface.Name} has output type {outputType.Name} that does not end with 'Output'");
}
// Validate namespace
if (!inputType.Namespace?.StartsWith("ConnectSoft.MicroserviceTemplate.DomainModel", StringComparison.Ordinal) ?? false)
{
Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that is not in DomainModel namespace");
}
}
}
}
Best Practices¶
Do's¶
-
Write Tests for All Architectural Rules
-
Use Descriptive Test Names
-
Include Failing Types in Error Messages
-
Handle Conditional Compilation
-
Group Related Tests
Don'ts¶
-
Don't Skip Architecture Tests
-
Don't Make Tests Too Permissive
// ❌ BAD - Too specific, misses violations Types.InAssembly(assembly) .That() .HaveName("MyController") // Too specific! .ShouldNot() .HaveDependencyOn("Repository") // ✅ GOOD - Comprehensive coverage Types.InAssembly(assembly) .That() .ResideInNamespace("Controllers") .ShouldNot() .HaveDependencyOnAny(allRepositoryAssemblies) -
Don't Ignore Test Failures
-
Don't Use Generic Error Messages
Integration with Clean Architecture¶
Enforcing Clean Architecture Rules¶
Architecture tests enforce Clean Architecture principles:
- Dependency Rule: Dependencies flow inward (outer → inner)
- Independence Rule: Inner layers don't depend on outer layers
- Interface Rule: Dependencies point toward abstractions
- Framework Independence: Domain layer has no framework dependencies
Layer Validation¶
Validate Each Layer:
// Domain Layer: No dependencies on infrastructure
[TestMethod]
public void DomainLayerShouldNotDependOnInfrastructure()
{
var result = Types.InAssembly(typeof(MicroserviceAggregateRootEntity).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.EntityModel")
.ShouldNot()
.HaveDependencyOnAny(
"NHibernate",
"MongoDB",
"Microsoft.AspNetCore")
.GetResult();
Assert.IsTrue(result.IsSuccessful);
}
// Application Layer: Only depends on Domain
[TestMethod]
public void ApplicationLayerShouldOnlyDependOnDomain()
{
var result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
.That()
.ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
.ShouldNot()
.HaveDependencyOnAny(
"ConnectSoft.MicroserviceTemplate.ServiceModel",
"NHibernate",
"MongoDB")
.GetResult();
Assert.IsTrue(result.IsSuccessful);
}
Troubleshooting¶
Issue: Test Fails Unexpectedly¶
Symptom: Test fails but code appears correct.
Solutions:
1. Check if test is too strict
2. Verify conditional compilation directives
3. Inspect FailingTypes to see which types fail
4. Check for transitive dependencies
Issue: Test Too Permissive¶
Symptom: Test passes but violations exist.
Solutions: 1. Review test logic for gaps 2. Add more specific conditions 3. Check for missing forbidden dependencies 4. Verify namespace matching
Issue: Performance Issues¶
Symptom: Tests run slowly.
Solutions: 1. Narrow scope with namespace filtering 2. Cache type queries if used multiple times 3. Use specific type filters instead of broad queries 4. Consider splitting large tests into smaller ones
Complete Test Coverage¶
Test Categories Summary¶
| Category | Test Count | Purpose |
|---|---|---|
| Layered Architecture | 8 tests | Enforce Clean Architecture dependency rules |
| Options Pattern | 4 tests | Validate options implementation |
| Naming Conventions | 9 tests | Ensure consistent naming across solution |
| Domain Events | 2 tests | Validate domain event placement and isolation |
| Domain Services | 4 tests | Enforce domain service pattern |
| Use Cases | 5 tests | Validate use case pattern with Input/Output |
| Repository Pattern | 4 tests | Enforce repository interfaces and implementations |
| Messaging Model | 6 tests | Validate commands and events |
| Cross-Cutting Concerns | 4 tests | Validate exceptions, extensions, constants |
| Entity Model Isolation | 5 tests | Ensure entity model purity |
| Application Model Isolation | 2 tests | Validate application model isolation |
| Specification Pattern | 3 tests | Enforce specification pattern |
Total: 56+ architecture tests covering all major architectural concerns.
Test Execution Statistics¶
Running all architecture tests:
# Run all architecture tests
dotnet test --filter "TestCategory=Architecture Unit Tests"
# Expected output:
# Passed! - Failed: 0, Passed: 56, Skipped: 0, Total: 56
Summary¶
Architecture Tests in the ConnectSoft Microservice Template provide:
- ✅ Build-Time Enforcement: Architectural violations fail builds automatically
- ✅ Living Documentation: Tests serve as executable documentation of rules
- ✅ Prevent Architectural Drift: Catch violations before they spread
- ✅ Team Alignment: Shared understanding of architectural constraints
- ✅ Refactoring Safety: Confidence when refactoring
- ✅ CI/CD Integration: Automated validation in every build
- ✅ Comprehensive Coverage: 56+ tests covering all architectural concerns
- ✅ Pattern Enforcement: Tests for Clean Architecture, DDD, Repository, Specification, and Use Case patterns
- ✅ Naming Consistency: Automated validation of naming conventions
- ✅ Layer Isolation: Strict enforcement of dependency rules
- ✅ Interface Compliance: Validation of interface implementations
By following these patterns, teams can:
- Maintain Clean Architecture: Automated enforcement prevents violations
- Catch Issues Early: Build-time validation catches problems before code review
- Document Architecture: Tests serve as living documentation
- Enable Refactoring: Confidence to refactor knowing architecture is protected
- Enforce Consistency: Consistent architecture across all projects
- Prevent Technical Debt: Architectural violations are caught immediately
- Onboard New Developers: Tests serve as examples of architectural rules
The Architecture Tests ensure that architectural principles are not just documented but actively enforced, preventing architectural degradation over time and maintaining the integrity of Clean Architecture throughout the microservice's lifecycle. With 56+ tests covering layered architecture, naming conventions, design patterns, and isolation rules, the template provides comprehensive architectural validation that scales with the codebase.
Related Documentation¶
- Microservice Template Architecture: Template-specific architecture model and enforcement implementation
- Clean Architecture: Architectural principles and patterns
- Domain-Driven Design: Domain layer organization
- Solution Structure: Template solution structure and project organization
- Unit Testing: Unit testing patterns and best practices