Why Do You Need To Write Architecture Tests in .NET
Learn why architecture tests matter in .NET, how they stop your layers from drifting, and how tools like NetArchTest and ArchUnitNET keep your design safe.
The cricket team rule
Imagine you are the coach of a small cricket team in your colony. You make a simple rule before every match. The batsmen bat, the bowlers bowl, and the wicketkeeper stays behind the stumps. Everyone nods. The plan is clear.
But during a busy match, things slip. A bowler wanders off to chat near the boundary. A batsman picks up the ball and starts bowling for fun. Nobody planned this. It just happened, one small step at a time, because no one was watching every player every minute.
Now imagine you had an umpire whose only job was to blow a whistle the second a player stood in the wrong spot. You would not need to watch everyone yourself. The whistle would catch the mistake right away, before it spread and lost you the match.
A .NET solution is like that cricket team. You decide which code is allowed to talk to which other code. You draw a neat plan. But as months pass and new people join, the plan slips, one small line of code at a time. An architecture test is your umpire. It blows the whistle the moment someone breaks a rule, so your design stays the shape you wanted.
This post is about the why. Why do these tests matter so much? What do they protect you from? And how do a couple of friendly .NET libraries make them easy to write?
What an architecture test actually is
When people draw a software design, they draw boxes and arrows. The boxes are projects or layers. The arrows say "this part is allowed to use that part." The whole design is really a set of rules about direction: who may depend on whom.
The problem is that a drawing on a whiteboard cannot stop anybody. The code still compiles even when the arrows are broken. A new teammate, in a hurry, adds one line that lets the core business code call the database directly. Everything builds. Every unit test still passes. The drawing on the wall still looks correct. But the real shape of the code has quietly drifted away from the plan.
An architecture test turns those arrows into runnable rules. A normal unit test checks behaviour: given this input, do I get the right answer? An architecture test checks shape: is the code arranged the way we agreed?
The lovely part is that it is just a test. It lives in your test project next to your unit tests. It runs with dotnet test. When it fails, it fails red like any other test, with a clear message about which class broke the rule. There is no special tool to learn and no separate dashboard to watch.
Why normal tests are not enough
Here is the trap many teams fall into. They have hundreds of unit tests. The coverage number looks great. The build is green. So they assume the code is healthy. But unit tests only check behaviour, and behaviour is only half the story.
Think of a class that calculates a price. It takes an order and returns a number. Your unit test gives it an order and checks the number. Perfect. But that same class might secretly reach into the database, or call a web service, or depend on a layer it should never touch. The number it returns is still correct, so the unit test still passes. The mistake is invisible to behaviour tests.
This is the key idea: a class can give the right answer while sitting in completely the wrong place. Only a test that looks at the wiring between projects can catch that.
Two kinds of tests, two different questions
Steps
Unit test
Asks: right answer?
Architecture test
Asks: right place?
Both green
Correct AND well shaped
So architecture tests do not replace unit tests. They sit beside them and guard a part of your code that unit tests are blind to.
The slow drift problem
Let me explain the real enemy. It is not one big mistake. It is a thousand tiny ones.
On day one, your solution is clean. The Domain project has no idea the database exists. The Web project knows nothing about how data is stored. Everyone is happy. But software is alive. People add features every week. They are busy and under pressure. One day someone needs a value quickly and adds a reference from Domain to the database project, because it works and the deadline is near.
Nobody notices. The code compiles. The tests pass. Six months later, the Domain project is tangled into the database, the web framework, and three external libraries. Nobody decided this. It happened by drift, one harmless-looking line at a time. Now refactoring is scary and expensive, because pulling one thread might unravel the whole sweater.
An architecture test stops drift at line number one. The first time someone adds the forbidden reference, the build turns red. The teammate sees the message immediately, while the change is fresh in their head, and fixes it in seconds. The tangle never gets a chance to grow.
This is the single biggest reason to write architecture tests: they make your architecture self-checking, so it cannot quietly rot.
Architecture as living documentation
There is a second reason that is just as valuable. A diagram on a wiki page goes stale the moment someone breaks it, and nobody updates the page. So your documentation slowly starts lying to new joiners.
Architecture tests cannot lie. If the test says "Domain must not depend on Infrastructure" and that test is green, then it is true right now, today, in the actual code. The test is documentation that is checked by the computer on every single build. New teammates can read the test names and learn the rules of the codebase in a few minutes.
Here is what a typical set of rules looks like, written with NetArchTest:
using NetArchTest.Rules;
using Xunit;
public class ArchitectureTests
{
private const string Domain = "MyApp.Domain";
private const string Infrastructure = "MyApp.Infrastructure";
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types.InAssembly(typeof(Order).Assembly)
.That()
.ResideInNamespace(Domain)
.ShouldNot()
.HaveDependencyOn(Infrastructure)
.GetResult();
Assert.True(result.IsSuccessful, FormatFailures(result));
}
private static string FormatFailures(TestResult result) =>
result.IsSuccessful
? string.Empty
: "Forbidden dependencies in: " +
string.Join(", ", result.FailingTypeNames);
}Read that test out loud. "Types in the Domain namespace should not have a dependency on Infrastructure." That sentence is your architecture rule. The fluent API was built so the code reads almost like plain English, which is exactly why it works so well as documentation.
Common rules worth protecting
Architecture tests are not only about layers. They can protect many small habits that keep a team consistent. Here are the most common ones, with what each one buys you.
| Rule | What it checks | Why it matters |
|---|---|---|
| Layer direction | Domain must not use Infrastructure | Keeps the core pure and testable |
| Naming | Handlers end in Handler | Easy to find and reason about code |
| Interfaces | Services live behind an interface | Makes swapping and mocking simple |
| Sealed types | Entities are sealed where intended | Prevents surprise inheritance |
| Module boundaries | Module A does not call Module B internals | Keeps a modular monolith honest |
You do not need all of these on day one. Pick the two or three that matter most for your project, and add more as the team agrees on new habits. The cost of each extra rule is tiny.
How to introduce architecture tests to a team
Steps
Agree rule
Pick one clear rule
Write test
One fluent test
Run in CI
Fails build on break
Add more
Grow slowly
Where these tests run: the feedback loop
The power of an architecture test comes from when it runs. It runs on every build, both on your machine and in your continuous integration (CI) pipeline. That short loop between writing code and getting feedback is what kills drift.
Because the feedback is so fast, the fix is cheap. The teammate has not yet moved on to another task. The forbidden line is still on their screen. They delete it or fix the design, and the tangle never forms. Compare that with finding the same problem six months later in a code review, when nobody remembers why the line was added.
Two friendly libraries
You do not have to build this machinery yourself. Two well-known .NET libraries do the heavy lifting.
NetArchTest is the small, friendly one. It has a fluent API that reads like English, and it covers most everyday rules about namespaces and dependencies. It is the easiest place for a beginner to start. One note from the community: the original BenMorris/NetArchTest package has been quiet since 2023, so some teams use a maintained fork called NetArchTest.eNhancedEdition, which keeps almost the same API and fixes several bugs.
ArchUnitNET is the bigger, richer one. It is the C# cousin of the famous Java library ArchUnit. It lets you define full layers, check slices, inspect class members, validate attributes, and even derive rules from PlantUML diagrams. Choose it when your rules grow beyond simple namespace checks.
Here is the same "no forbidden dependency" idea written with ArchUnitNET:
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
public class LayerTests
{
private static readonly Architecture Arch =
new ArchLoader()
.LoadAssemblies(typeof(Order).Assembly)
.Build();
private readonly IObjectProvider<IType> DomainLayer =
Types().That().ResideInNamespace("MyApp.Domain");
private readonly IObjectProvider<IType> InfraLayer =
Types().That().ResideInNamespace("MyApp.Infrastructure");
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
IArchRule rule = Types()
.That().Are(DomainLayer)
.Should().NotDependOnAny(InfraLayer);
rule.Check(Arch);
}
}The two libraries look different, but the goal is identical: write the rule once, and let it guard your design forever. A simple way to choose is shown below.
| Need | NetArchTest | ArchUnitNET |
|---|---|---|
| Tiny, easy API | Best fit | Heavier |
| Namespace and dependency rules | Yes | Yes |
| Full layer definitions | Limited | Strong |
| Member-level checks | No | Yes |
| Rules from diagrams | No | Yes (PlantUML) |
Many teams begin with NetArchTest for a few quick wins and only move to ArchUnitNET when their needs outgrow it.
You can also protect naming habits, not just dependencies. Here is a small NetArchTest rule that keeps every handler class ending in Handler, so the team can always find them:
[Fact]
public void Handlers_Should_End_With_Handler()
{
var result = Types.InAssembly(typeof(Order).Assembly)
.That()
.ImplementInterface(typeof(IRequestHandler<,>))
.Should()
.HaveNameEndingWith("Handler")
.GetResult();
Assert.True(result.IsSuccessful);
}A rule like this looks tiny, but it saves real time. New joiners learn the naming habit from the test instead of from a long style document nobody reads.
A common worry: do these tests slow me down?
Almost never. Architecture tests load your already-compiled assemblies and inspect the types in memory. There is no database, no web server, and no network call. A whole suite of them usually finishes in well under a second. You can safely run them on every push without anyone noticing the extra time.
The bigger worry is the opposite: a rule that is too strict can become annoying and people start ignoring it. The cure is to keep rules meaningful. Each test should protect a real decision the team cares about, not a personal style preference. When a rule no longer makes sense, delete it, just like you would delete a stale comment.
A note on third-party libraries
While you design your dependency rules, it helps to know which outside libraries you are leaning on. A few popular .NET libraries changed their licensing recently. For example, MediatR and MassTransit are now under commercial licenses for many uses. This does not change how architecture tests work, but it is a good reason to keep your core Domain free of such dependencies. If your Domain stays pure, a licensing change in an outer library never touches the heart of your application. Architecture tests are exactly how you keep that promise honest.
Quick recap
- An architecture test checks the shape of your code, not its behaviour. It asks "is my code in the right place?" rather than "did I get the right answer?"
- Unit tests cannot catch wrong dependencies, because a class can give a correct answer while sitting in the wrong layer. You need both kinds of test.
- The real enemy is slow drift: many tiny, harmless-looking changes that turn a clean design into a tangle over months.
- Architecture tests stop drift at line one by failing the build the moment a rule is broken, while the change is still fresh.
- They double as living documentation that the computer checks on every build, so they can never quietly go stale.
- NetArchTest is the friendly starting point; ArchUnitNET is the richer tool for bigger needs.
- These tests are fast, running in well under a second, so they cost almost nothing to keep in your pipeline.
- Keep your Domain pure so that outside changes, even licensing changes in libraries like MediatR or MassTransit, never reach your core.
References and further reading
- NetArchTest on GitHub (BenMorris/NetArchTest)
- ArchUnitNET on GitHub (TNG/ArchUnitNET)
- ArchUnitNET documentation (Read the Docs)
- Why Do You Need To Write Architecture Tests in .NET — antondevtips
- Enforcing Software Architecture With Architecture Tests — Milan Jovanović
- Writing ArchUnit style tests for .NET and C# — Ben Morris
Related Posts
Enforcing Software Architecture With Architecture Tests in .NET
Learn how to enforce software architecture in .NET using architecture tests with NetArchTest and ArchUnitNET, so your layers and rules stay safe over time.
5 Architecture Tests You Should Add to Your .NET Projects
Five simple architecture tests for .NET using NetArchTest. Protect layers, naming, and dependencies with code that fails the build when rules break.
Shift Left With Architecture Testing in .NET
Learn how to shift left with architecture testing in .NET using NetArchTest and ArchUnitNET, so bad dependencies are caught early before they reach production.
The Test Pyramid Is a Lie (And What I Do Instead) in .NET
The test pyramid says write mostly unit tests. In real .NET apps that often backfires. Here is the testing trophy and how I balance tests instead.
Building Your First Use Case With Clean Architecture in .NET
A beginner-friendly, step-by-step guide to building your first use case in .NET Clean Architecture: command, handler, repository, and endpoint, with diagrams.
CQRS Pattern with MediatR in .NET: A Friendly Guide
Learn the CQRS pattern with MediatR in .NET using simple words, clear diagrams, and real C# code. Beginner friendly, with pitfalls and licensing notes.