Sitemap

Architecture Tests

5 min readSep 27, 2025

--

Press enter or click to view image in full size

Architecture tests are an essential part of maintaining a healthy architecture. First time hearing? No worries, we will explore what those tests are, why they matter, and how to implement them effectively using practical examples in C#.

There are also other articles on this topic you might find interesting:

If you are ready, we can start!

Demystifying Architecture Tests

While unit, integration, and E2E tests aim to validate functional requirements, architecture tests focus on enforcing code conventions and architecture.

But wait. What is architecture? 🤔

Software architecture is the structure of a system, its components, and the relationship between them.

Press enter or click to view image in full size

So, how do you ensure the system adheres to agreed principles? Like “controllers cannot depend on repositories directly”.

By writing architecture tests 😎!

They accelerate code reviews, help developers avoid repetitive mistakes, and make the team leader happy 🥺.

Types of Architecture Tests

Let’s say you want to validate that your services do not depend on UI.

Architectural rules can be validated at different points in the development process, depending on how early you want to catch violations and how strictly you want to enforce them:

  • Runtime checks — run as part of your automated test suite (e.g., alongside unit tests).

It is common to use libraries like NetArchTest or ArchUnitNET for this purpose. It allows you to express architectural rules in unit tests.

[Fact]
public void Services_should_not_depend_on_UI()
{
var result = Types
.InCurrentDomain()
.That()
.ResideInNamespace("MyApp.Services")
.ShouldNot()
.HaveDependencyOn("MyApp.UI")
.GetResult();

Assert.True(result.IsSuccessful, "Services layer should not depend on UI layer.");
}

Those kinds of tests are easy to write; however, feedback comes later (in test run, CI/CD pipeline, and not during coding).

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

Additionally, less experienced developers might rewrite those tests just to make their code pass 😑.

  • Compile-time checks — performed during compilation, often with Roslyn analyzers.

They prevent violations from even building, giving developers instant feedback in their IDE

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NoUIDependencyInServicesAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
"ARCH001",
"Service layer must not depend on UI layer",
"Type '{0}' in Services depends on UI",
"Architecture",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(symbolContext =>
{
var namedType = (INamedTypeSymbol)symbolContext.Symbol;

if (namedType.ContainingNamespace.ToString().Contains("MyApp.Services") &&
namedType.AllInterfaces.Any(i => i.ContainingNamespace.ToString().Contains("MyApp.UI")))
{
symbolContext.ReportDiagnostic(
Diagnostic.Create(Rule, namedType.Locations[0], namedType.Name));
}
}, SymbolKind.NamedType);
}
}

With a Roslyn analyzer like that, the violation is shown in the IDE as you type, and the build fails before the code can run.

Press enter or click to view image in full size

But those are usually harder to write, maintain, and when changed, have to be shipped to every team member.

🔹Notice, many teams use both: compile-time rules for must-follow constraints and runtime tests for broader structure validation.

Targets for Architecture Testing

Architecture tests help ensure that your codebase adheres to agreed design principles, conventions, and constraints, keeping the system maintainable and consistent as it grows.

For example, those are typical tests you would write:

class ArchTests
{
private Assembly DomainAssembly = typeof(IEntity).Assembly;
private Assembly InfrastructureAssembly = typeof(AppDbContext).Assembly;

[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types
.InAssembly(DomainAssembly)
.ShouldNot()
.HaveDependencyOn(InfrastructureAssembly.GetName().Name)
.GetResult();

Assert.True(result.IsSuccessful);
}

}
  • naming convention — ensure all classes in a namespace end with the correct suffix or start with the correct prefix:
[Fact]
public void Services_Should_Have_Correct_Naming()
{
var result = Types
.InNamespace(ApplicationAssembly)
.That()
.ImplementInterface(typeof(IService))
.Should()
.HaveNameEndingWith("Service")
.GetResult();

Assert.True(result.IsSuccessful);
}

[Fact]
public void CommandHandler_Should_Have_Correct_Naming() { . . . }

[Fact]
public void Exceptions_Should_Have_Name_Endign_With_Exception() { . . . }

[Fact]
public void Repositories_Should_Have_Name_Endign_With_Repository() { . . . }

[Fact]
public void DomainEvents_Should_Have_DomainEvent_Postfix() { . . . }

🔹Notice, following consistent conventions is helpful not only to match team preferences but also because reflection can scan your assemblies based on naming patterns.

  • access modifiers — validate public, public, protected are properly used (only public types are exposed in a specific namespace):
[Fact]
public void Infrastructure_Implementations_Should_Be_Private()
{
var result = Types
.InNamespace(InfrastructureAssembly)
.Should()
.NotBePublic();

Assert.True(result.IsSuccessful);
}

[Fact]
public void Services_Implementations_Should_Be_Private()
{
var result = Types
.InCurrentDomain()
.That()
.ResideInNamespaceStartingWith("Prefix.Services")
.And()
.AreClasses()
.And()
.HaveNameEndingWith("Services")
.Should()
.NotBePublic();

Assert.True(result.IsSuccessful);
}

[Fact]
public void CommandHandler_Should_NotBe_Public() { . . . }

[Fact]
public void AllDTOs_Should_Have_GetAndInit_And_Be_Immutable() { . . . }

[Fact]
public void BaseClasses_Should_Be_Abstract() { . . . }
  • universal type — ensure all classes inherit the same base type
[Fact]
public void Exceptions_Should_Inherit_MyBaseException()
{
var result = Types
.GetAssemblies("MyCompany*.dll")
.That()
.Inherit(typeof(System.Exception))
.Should()
.Inherit(typeof(MyBaseException))
.GetResult();

Assert.True(result.IsSuccessful);
}

[Fact]
public void Entities_Should_Inherit_BaseEntity() { . . . }

The options are limitless, and it’s your choice how many design rules you decide to enforce — whether it’s forbidding direct HttpClient usage, banning DateTime.Now in favor of IDateTimeProvider, ensuring all enums define an Unknown=0 value, or that commands do not return a value.

Sum It Up

Architecture tests play a crucial role in maintaining the integrity and consistency of a software system as it evolves.

They enforce design principles and constraints by validating structural rules in code — like naming conventions, dependency boundaries, inheritance hierarchies, and contract adherence.

💬 Let me know if you are interested in hearing about performance tests. They measure things like latency, throughput, and resource usage to make sure your architecture is sufficient and satisfies your needs

👏 Don’t forget to give the article a rating if you liked it

☕️ You can support me using the link below

😉 See you around

--

--

iamprovidence
iamprovidence

Written by iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence