You write unit tests to validate behavior - but what if you could write tests to enforce architecture?
Here are 5 architectural tests:
1️⃣ Ensure Domain Layer does not reference Infrastructure
Prevent accidental dependency from domain (core business logic) to infrastructure (e.g., DB access, APIs). Key in Clean Architecture, Onion Architecture, and Hexagonal Architecture.
Use Case: someone adds a logging service or EF Core entity in the domain layer - this test catches that.
[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
var domainAssembly = typeof(MyDomainRoot).Assembly;
var domainReferences = domainAssembly.GetReferencedAssemblies();
Assert.DoesNotContain(domainReferences, a => a.Name.Contains("Infrastructure"));
}
2️⃣ Ensure Application Services don't reference Infrastructure Namespace
Prevent internal leakage of infrastructure code into application-level service logic. Keeps the application layer technology-agnostic.
Use Case: a developer adds an Infrastructure.DbContext reference inside Application.Services.UserService - this test detects that immediately.
[Fact]
public void ApplicationServices_Should_Not_Use_Infrastructure()
{
var applicationTypes = Assembly
.GetAssembly(typeof(Application.Services.SomeService))
.GetTypes()
.Where(t => t.Namespace?.StartsWith("Application.Services") == true);
foreach (var type in applicationTypes)
{
var referencedAssemblies = type.Assembly.GetReferencedAssemblies();
foreach (var asm in referencedAssemblies)
{
Assert.False(asm.Name.Contains("Infrastructure"), $"{type.Name} references Infrastructure");
}
}
}
3️⃣ All IRequestHandlers should be Internal
Enforces encapsulation by ensuring CQRS/MediatR handlers are not accidentally made public. This avoids misuse of internal logic by external consumers.
Use Case: your project is becoming a NuGet SDK or you're layering your app - this ensures only what should be public is exposed.
[Fact]
public void AllRequestHandlers_Should_Be_Internal()
{
var handlerType = typeof(MediatR.IRequestHandler<,>);
var types = typeof(SomeRequest).Assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == handlerType));
foreach (var type in types)
{
Assert.True(type.IsNotPublic, $"{type.Name} should be internal");
}
}
4️⃣ Ensure all Public API members have Description
Validates that SDK-facing code or public API surface is properly described. Helps enforce consistency and maintainability in public projects.
Use Case: building a client SDK or public-facing API that requires all exported classes and members to have specific Description.
❗ Can be used for different Attributes. For XML specific documentation use linters and static code analyzers.
[Fact]
public void PublicMembers_Should_Have_Xml_Documentation()
{
var types = typeof(MySdkRoot).Assembly.GetExportedTypes();
foreach (var type in types)
{
foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
var docs = member.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
Assert.True(docs.Any(), $"{member.Name} in {type.Name} lacks documentation");
}
}
}
5️⃣ Forbid DateTime.Now usage
Prevent usage of DateTime.Now in business logic; enforce usage of UTC format, TimeProvider or other abstractions for better testability and time consistency.
Use Case: in production, code using DateTime.Now may behave unpredictably (e.g., daylight saving).
ℹ️ Uses Mono.Cecil for inspecting IL: github.com/jbevain/cecil
using Mono.Cecil;
[Fact]
public void No_Class_Should_Use_DateTimeNow()
{
// Arrange
var assembly = Assembly.GetAssembly(typeof(MyApp.Domain.SomeDomainClass));
var module = ModuleDefinition.ReadModule(assembly.Location);
var offenders = new List();
// Act
foreach (var type in module.Types)
{
foreach (var method in type.Methods)
{
if (!method.HasBody)
continue;
if (method.Body.Instructions.Any(instr =>
instr.OpCode == Mono.Cecil.Cil.OpCodes.Call &&
instr.Operand is MethodReference mr &&
mr.FullName.Contains("System.DateTime::get_Now")))
{
offenders.Add($"{type.FullName}.{method.Name}");
}
}
}
// Assert
Assert.True(offenders.Count == 0,
$"Forbidden DateTime.Now usage found in: {string.Join(", ", offenders)}");
}
💡 These tests protect code architecture boundaries, encapsulation, and long-term maintainability - things not enforceable by analyzers alone.
They are especially useful in:
- Domain-driven design projects
- Clean Architecture and modular monoliths
- Public SDKs
- Mission-critical systems with strict testability requirements