We are continuing our public webinars through 2025! This year’s webinar series kicked off with a webinar on Architecture Testing for .NET.
In this webinar, we talk about architecture testing, its benefits, and how you can get started with automating some architectural tests in .NET using the TngTech.ArchUnitNET package.
Please watch the webinar if you have questions on how these tests are built. We show them in Visual Studio and point out key points in the webinar.
In this post, I want to share the code part of getting started with ArchUnitNET, including the never shown access test.
Demo Project: eShopOnWeb
For our code demos, I decided to use eShopOnWeb. The reason I chose this solution is to show how you can bring in architecture testing on a legacy code project and use the architecture tests to find potential issues. You can follow along by either downloading the source or forking it and working in your fork.
This solution already uses xUnit for its testing framework. As ArchUnitNET supports MsTest, xUnit, and NUnit, we’ll show how it works with xUnit.
The code for this webinar is located in the sadukie/ArchUnitNET-tests branch.
In the tests
folder, create a new xUhit Testing Project and call it ArchitectureTests
.
Install the Packages
Once the ArchitectureTests
project is created, add the following NuGet packages:
TngTech.ArchUnitNET
TngTech.ArchUnitNET.xUnit
Note: eShopOnWeb uses central package management. If you run into issues with the version showing up in your .csproj
file, you’ll need to update your entries. The version used in the demos is 0.11.1.
Remember the Usings
For most of the tests, you will need to reference at least one or more of the following usings:
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
Create the Architecture
Once you have the project created and the references added, then you can create the lines to set up your architecture and the layers used for testing.
In my demo, I used a helper class so that I can use it across files.
This is what my architecture declaration looked like:
private static System.Reflection.Assembly coreAssembly = typeof(IOrderService).Assembly;
private static System.Reflection.Assembly blazorAdminAssembly = typeof(HttpService).Assembly;
private static System.Reflection.Assembly blazorSharedAssembly = typeof(ICatalogItemService).Assembly;
private static System.Reflection.Assembly infrastructureAssembly = typeof(FileItem).Assembly;
private static System.Reflection.Assembly publicAPIAssembly = typeof(BaseMessage).Assembly;
private static System.Reflection.Assembly webAssembly = typeof(GetOrderDetails).Assembly;
internal static readonly Architecture Architecture = new ArchLoader()
.LoadAssemblies(
coreAssembly,
blazorAdminAssembly,
blazorSharedAssembly,
infrastructureAssembly,
publicAPIAssembly,
webAssembly
)
.Build();
Create the Layers
I created references to each of the assemblies and then loaded those for my architecture. I also created the layers individually since I do want to do layer testing. Each layer is a type of IObjectProvider<IType>
. This is what those look like:
internal static readonly IObjectProvider<IType> CoreLayer = Types()
.That()
.ResideInAssembly(coreAssembly)
.As("Core Layer");
internal static readonly IObjectProvider<IType> BlazorAdminLayer = Types()
.That()
.ResideInAssembly(blazorAdminAssembly)
.As("BlazorAdmin Layer");
internal static readonly IObjectProvider<IType> BlazorSharedLayer = Types()
.That()
.ResideInAssembly(blazorSharedAssembly)
.As("BlazorShared Layer");
internal static readonly IObjectProvider<IType> InfrastructureLayer = Types()
.That()
.ResideInAssembly(infrastructureAssembly)
.As("Infrastructure Layer");
internal static readonly IObjectProvider<IType> PublicAPILayer = Types()
.That()
.ResideInAssembly(publicAPIAssembly)
.As("API Layer");
internal static readonly IObjectProvider<IType> WebLayer = Types()
.That()
.ResideInAssembly(webAssembly)
.As("Web Layer");
The .As()
text is used in error messages, so make sure to use names that make sense.
Create Tests
There are many tests that can be created and automated to ensure that your architectural guidance is being carried throughout the code. When I’m dropped into a codebase, I look for the easy starts. This includes naming tests, layer dependency tests, and access tests.
Tests operate on two concepts - rule and check. The rule states the rule that must be enforced. The check ensures that the rule is enforced within the architecture.
Rules have a bunch of different starting points, including:
- Types
- Classes
- Interfaces
- Members
- Attributes
- Slices
… and more! These are defined in ArchUnitNET.Fluent.ArchRuleDefinition
.
Naming and Implementation Tests
I’m going to show a few different approaches here. First, let’s say that all classes that implement an IRepository<T>
must have Repository in their name.
Implementing architecture rules around generics can get tricky. You can’t use typeof(IRepository<T>)
. So instead of using the typeof()
syntax, we will take advantage of the regular expressions approach that ArchUnitNET uses. Our rule will read like this:
- All classes
- That
- Are assignable to interfaces in the
Microsoft.eShopWeb.ApplicationCore.Interaces.IRepository*
namespace - and settrue
at the end to indicate that we’re using regular expressions here - Should
- Have a name containing “Repository”
Once we build the rule, then let’s check to see if our architecture is following the rule. Here’s the test:
[Fact]
public void IRepositoryImplementersShouldIncludeRepositoryInName()
{
IArchRule repositoryImplementerNameShouldIncludeRepository =
Classes()
.That()
.AreAssignableTo("Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*", true)
.Should()
.HaveNameContaining("Repository");
repositoryImplementerNameShouldIncludeRepository.Check(LayerHelper.Architecture);
}
Now maybe you have to work with devs who are in a habit of naming things but forgetting the implementation part. Consider this rule:
- All classes
- That
- Have a name containing “Repository”
- Should
- Implement a repository in the
Microsoft.eShopWeb.ApplicationCore.Interaces.IRepository*
namespace - and settrue
at the end to indicate that we’re using regular expressions here
This is the test:
[Fact]
public void ClassesNamedAsRepositoryShouldImplementIRepository()
{
IArchRule classesNamedAsRepositoryShouldImplementIRepository =
Classes()
.That()
.HaveNameContaining("Repository")
.Should()
.ImplementInterface("Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*",true);
classesNamedAsRepositoryShouldImplementIRepository.Check(LayerHelper.Architecture);
}
Now you might be looking at eShopOnWeb, searching for Repository
in Solution Explorer and notice that there’s 1 class a couple interfaces with “Repository” in their names. The rules we’ve covered so far only apply to the classes. We can create a test for interfaces as well. Notice in this case that the starting point is Interfaces()
instead of Classes()
.
[Fact]
public void InterfacesNamedAsRepositoryShouldImplementIRepository()
{
IArchRule interfacesNamedAsRepositoryShouldImplementIRepository =
Interfaces()
.That()
.HaveNameContaining("Repository")
.Should()
.ImplementInterface("Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*", true);
interfacesNamedAsRepositoryShouldImplementIRepository.Check(LayerHelper.Architecture);
}
When you run the interfaces test, you should see a failure message like this:
ArchUnitNET.xUnit.FailedArchRuleException : "Interfaces that have name containing "Repository" should implement interface with full name matching "Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*"" failed:
Microsoft.eShopWeb.ApplicationCore.Interfaces.IReadRepository`1 does not implement interface with full name matching "Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*"
Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository`1 does not implement interface with full name matching "Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*"
Microsoft.eShopWeb.ApplicationCore.Interfaces.IWriteRepository does not implement interface with full name matching "Microsoft.eShopWeb.ApplicationCore.Interfaces.IRepository*"
Stack Trace:
ArchRuleAssert.CheckRule(Architecture architecture, IArchRule archRule)
ArchRuleExtensions.Check(IArchRule archRule, Architecture architecture)
NamingTests.InterfacesNamedAsRepositoryShouldImplementIRepository() line 57
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
This is true because none of the interfaces implement IRepository<T>
using the regex statement that we threw at it.
Dependency Tests
Another type of test you might want to consider are dependency tests. Clean Architecture is one of the big topics we talk about, and it has rules about how the layers interact. So you could write rules for each of those layers. We do that for our Core layer demo. For each of the architecture rules that we bulid, we make sure to use the Because()
clause to explain why these rules apply.
IArchRule coreShouldNotDependOnWeb = Types()
.That()
.Are(LayerHelper.CoreLayer)
.Should()
.NotDependOnAny(LayerHelper.WebLayer)
.Because("Core and Web should be independent of each other.");
IArchRule coreShouldNotDependOnBlazorAdmin = Types()
.That()
.Are(LayerHelper.CoreLayer)
.Should()
.NotDependOnAny(LayerHelper.BlazorAdminLayer)
.Because("Core and Blazor Admin should be independent of each other.");
IArchRule coreShouldNotDependOnBlazorShared = Types()
.That()
.Are(LayerHelper.CoreLayer)
.Should()
.NotDependOnAny(LayerHelper.BlazorSharedLayer)
.Because("Core and Blazor Shared should be independent of each other.");
IArchRule coreShouldNotDependOnInfrastructure = Types()
.That()
.Are(LayerHelper.CoreLayer)
.Should()
.NotDependOnAny(LayerHelper.InfrastructureLayer)
.Because("Core and Infrastructured should be independent of each other.");
IArchRule coreShouldNotDependOnPublicAPI = Types()
.That()
.Are(LayerHelper.CoreLayer)
.Should()
.NotDependOnAny(LayerHelper.PublicAPILayer)
.Because("Core and API should be independent of each other.");
Combining Rules
We can run each of these separately by calling .Check()
on each of the rules. However, sometimes, you may find that you have a combination of rules that apply. You can use And()
and Or()
and parentheses for determining the order of rules when combining them. For our ApplicationCore layer in eShopOnWeb, it should not depend on any of the other layers. This test ensures that all of these rules are applied and true:
[Fact]
public void CoreShouldNotDependOnExternalLayers()
{
// All these must be met
coreShouldNotDependOnBlazorAdmin
.And(coreShouldNotDependOnBlazorShared)
.And(coreShouldNotDependOnInfrastructure)
.And(coreShouldNotDependOnPublicAPI)
.And(coreShouldNotDependOnWeb)
.Check(LayerHelper.Architecture);
}
Access Tests
Another easy thing to look into is ensuring that parts of your code are set to desired access modifiers. One of the basic object-oriented concepts is to keep fields private and accessible through properties. Let’s ensure that the entities in ApplicationCore have private fields.
This test was not shown in the webinar.
That test looks like this:
[Fact]
public void FieldsInEntitiesShouldBePrivate()
{
// For property members in entities
// Make sure their setters are set to private
FieldMembers()
.That()
.AreDeclaredIn(Classes().That().ResideInNamespace("Microsoft.eShopWeb.ApplicationCore.Entities*", true))
.Should()
.BePrivate()
.Check(LayerHelper.Architecture);
}
This test should pass, as the entities in this code only have private fields.
Identifying Potential Problems with Architecture Tests
When I first got into eShopOnWeb, I wanted to understand the code a bit better. If there’s anything I look for when dealing with websites with admin portals, it’s that I don’t want my public site dependent on the private admin portal. So the first architecture test I really wanted to run was to ensure that Web wasn’t reliant on BlazorAdmin. Here’s my test:
[Fact]
public void WebShouldNotDependOnBlazorAdmin()
{
IArchRule webShouldNotDependOnBlazorAdmin = Types()
.That()
.Are(LayerHelper.WebLayer)
.Should()
.NotDependOnAny(LayerHelper.BlazorAdminLayer)
.Because("BlazorAdmin and Web should be independent of each other.");
webShouldNotDependOnBlazorAdmin.Check(LayerHelper.Architecture);
}
This is the output I got:
ArchitectureTests.Web.WebLayerTests.WebShouldNotDependOnBlazorAdmin
Duration: 577 ms
Message:
ArchUnitNET.xUnit.FailedArchRuleException : "Types that are Web Layer should not depend on BlazorAdmin Layer because BlazorAdmin and Web should be independent of each other." failed:
Microsoft.eShopWeb.Web.Extensions.ServiceCollectionExtensions does depend on BlazorAdmin.ServicesConfiguration
Microsoft.eShopWeb.Web.Controllers.UserController does depend on BlazorAdmin.Authorization.UserInfo and BlazorAdmin.Authorization.ClaimValue
Stack Trace:
ArchRuleAssert.CheckRule(Architecture architecture, IArchRule archRule)
ArchRuleExtensions.Check(IArchRule archRule, Architecture architecture)
WebLayerTests.WebShouldNotDependOnBlazorAdmin() line 21
RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Seeing these results makes me want to look into these relationships further and see if we can refactor these into a shared library rather than a direct dependency between the web and admin sites.
Conclusion
In this webinar, we talked through the benefits of architecture testing as well as how to get started with it today.
If you’re interested in the links to learn more, you can sign up for that email here.
Consider automating architecture tests in your .NET solutions, knowing that your architectural guidance is being enforced consistently!