In this article, we’ll explore how to use the NetArchTest.Rules library to write architecture tests for our .NET applications.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Creating the Base for Architecture Tests in .NET

For this article, we’re going to use a simple project based on the Onion architecture:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Shows project structure of Architecture Tests in .NET

We create several projects representing the different layers of this architectural pattern.

If you want to find out more about the Onion Architecture, check out our article Onion Architecture in ASP.NET Core.

Firstly, the Domain layer consists of the Domain project. Moving on to the Service layer, we find the implementation split between the Services and Services.Abstractions projects. Transitioning to the Infrastructure layer, we have the Persistence project. Lastly, the Api project represents the Presentation layer.

Let’s create our base entity:

public class Cat
{
    public required Guid Id { get; set; }
    public required string Name { get; set; }
    public required string Breed { get; set; }
    public required DateTime DateOfBirth { get; set; }
}

Here, we create the Cat class around which our application is built. We add, update, delete, and query cats via the Api project.

Now that we’ve got our projects ready to test, let’s proceed by creating a new xUnit test project and install the required NuGet package:

dotnet add package NetArchTest.Rules

We can find the main rules for architecture tests in the NetArchTest.Rules package. NetArchTest itself is a library that allows us to create tests that enforce conventions for class design, naming, and dependency in .NET.

Now that we have everything ready, let’s start testing!

Architecture Tests to Prevent Inheritance in .NET

We know that we want the classes in the Persistence layer to be marked as sealed, thus ensuring that they cannot be inherited. Let’s see how we can enforce this:

[Fact]
public void GivenPersistenceLayerClasses_WhenInheritanceIsAttempted_ThenItShouldNotBePossible()
{
    // Arrange
    var persistenceLayerAssembly = typeof(CatsDbContext).Assembly;

    // Act
    var result = Types.InAssembly(persistenceLayerAssembly)
        .Should().BeSealed()
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Firstly, we get hold of the assembly for our Persistence project. Then, we start by using the Types class which is the entry point to the NetArchTest.Rules library. We move on to pass the persistenceLayerAssembly variable to the InAssembly() method – this will get all the types in that assembly.

Secondly, we call the Should() method. With it, we apply conditions to the list of matching types we have so far. In our case, the only condition we have is that all classes must be sealed, so we use the BeSealed() method. We finish off by calling the GetResult() method. 

Finally, we get a variable result having the TestResult type. It has one boolean IsSuccessful property that states whether or not all types match our conditions. Therefore, using the FluentAssertions library, we ensure that the IsSuccessful property returns true.

FluentAssertions are a great way to improve your unit tests, check out our article Improving Unit Tests with Fluent Assertions.

Architecture Tests That Enforce Class Visibility in .NET

In Onion architecture, concrete service implementations should not be visible outside the Service layer.

This doesn’t apply to the interfaces in the Services.Abstractions projects and all of them should be public, so let’s set up a test for that:

[Fact]
public void GivenServiceLayerInterfaces_WhenAccessedFromOtherProjects_ThenTheyAreVisible()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;

    // Act
    var result = Types.InAssembly(serviceLayerAssembly)
        .Should().BePublic()
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Using the IServiceManager type, we get the assembly where all the interface types in the Service layer reside. Then, we use the BePublic() method, which is the condition we are after. In the end, we assert that the result is successful.

Ensure Correct Implementation or Inheritance

We can easily ensure that our classes either implement the correct interfaces or inherit from the desired classes:

[Fact]
public void GivenCatNotFoundException_ThenShouldInheritFromNotFoundException()
{
    // Arrange
    var domainLayerAssembly = typeof(CatNotFoundException).Assembly;

    // Act
    var result = Types.InAssembly(domainLayerAssembly)
        .That().ResideInNamespace("Domain.Exceptions")
        .And().HaveNameStartingWith("Cat")
        .Should().Inherit(typeof(NotFoundException))
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

Here, we create a test to ensure that the CatNotFoundException class inherits from the NotFoundException class. After specifying the assembly, we use the That() method to specify to which types our conditions must apply. We use the ResideInNamespace() method to get all types in the Domain.Exceptions namespace.

This will get types in any child namespaces, so we add the And() method to add further filtering and continue with the HaveNameStartingWith() method. Only our CatNotFoundException matches all filters and we continue with the Should() and Inherit() methods. Finally, we assert that the result is true, confirming that our inheritance rules are abided. 

Architecture Tests That Enforce Project References in .NET

Software architectures such as either Onion or Clean architecture have strict rules when it comes to project dependencies. Let’s create a test to verify these strict rules:

[Fact]
public void GivenDomainLayer_ThenShouldNotHaveAnyDependencies()
{
    // Arrange
    var domainLayerAssembly = typeof(Cat).Assembly;

    // Act
    var result = Types.InAssembly(domainLayerAssembly)
        .ShouldNot().HaveDependencyOnAll(
            ["Api", "Contracts", "Persistence", "Services", "Services.Abstractions"])
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

First, we write a test to ensure that our Domain project doesn’t have any dependencies on the rest of the projects in our solution. Then, we get the required assembly and use the ShouldNot() method together with the HaveDependencyOnAll() method. To the latter, we pass all other namespaces as strings using collection expressions.

Note that many of the conditional methods in NetArchTest.Rules have two versions – either starting with Have or with NotHave. This, alongside the Should() and ShouldNot() methods, enables us to write tests in two different ways, achieving the same result.

Writing a Custom Rule for Architecture Tests in .NET

The NetArchTest.Rules library allows us to create custom rules as well:

public class CustomServiceLayerRule : ICustomRule
{
    public bool MeetsRule(TypeDefinition type)
        => type.IsInterface && type.IsPublic && type.Name.StartsWith('I');
}

To create a custom rule, we need to implement the ICustomRule interface and it’s MeetsRule() method. We create a rule to ensure that all types in the Services.Abstractions project are interfaces, have the public access modifier and the name is prefixed with I.

Now, let’s use our rule:

[Fact]
public void GivenServiceInterfaces_ThenShouldBePublicAndBeInterfacesAndStartWithI()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;
    var myCustomRule = new CustomServiceLayerRule();

    // Act
    var result = Types.InAssembly(serviceLayerAssembly)
        .Should().MeetCustomRule(myCustomRule)
        .GetResult();

    // Assert
    result.IsSuccessful.Should().BeTrue();
}

In our test, we first create an instance of our CustomServiceLayerRule class. Then, we pass that instance to the MeetCustomRule() method. This will apply this rule to all matching types, in our case those are all types in the Services.Abstractions project.

Defining Custom Rules Policies for Architecture Tests in .NET

The NetArchTest.Rules library allows us to utilize custom policies that combine several different rules:

[Fact]
public void GivenServiceInterfaces_ThenShouldMeetCustomRuleAndServiceManagerShouldHaveDependencyOnContracts()
{
    // Arrange
    var serviceLayerAssembly = typeof(IServiceManager).Assembly;
    var myCustomRule = new CustomServiceLayerRule();

    var customPolicy = Policy.Define(
            "Service Interfaces Policy",
            "This policy ensures that all types meet the given conditions")
        .For(Types.InAssembly(serviceLayerAssembly))
        .Add(types => types
            .Should().MeetCustomRule(myCustomRule))
        .Add(types => types
            .That().HaveNameEndingWith("Manager")
            .ShouldNot().HaveDependencyOn("Contracts")
        );

    // Act
    var results = customPolicy.Evaluate();

    // Assert
    foreach (var result in results.Results)
    {
        result.IsSuccessful.Should().BeTrue();
    }
}

Firstly, we start to create a custom policy by calling the Define() method on the Policy type. The method takes two parameters – a name and a description. We continue the policy creation with the For() method in which we specify which types we are going to test.

Next, we move on to defining rulesets for different sub-types. We do this by chaining Add() methods, in which we filter types and use the Should() and ShouldNot() methods to assert given conditions.

After our policy is done, we invoke the Evaluate() method to get a list of TestResult object. Finally, we assert that the IsSuccessful property of each result is true, ensuring all conditions have been met.

Conclusion

In this article, we delved into the effective utilization of the NetArchTest.Rules library to craft architecture tests specifically tailored to .NET applications. By enforcing conventions on class design, naming, and dependencies, we ensure the integrity of a given software architecture. Notably, the library provides a robust framework for writing effective architecture tests, addressing various aspects from preventing unwanted inheritance to validating project references and implementing custom rules. Furthermore, a significant feature is the ability to create policies, that enhance its flexibility and enable us to tailor tests to meet specific project requirements. Through the integration of NetArchTest.Rules into our testing workflow, we can confidently apply architectural best practices, fostering a resilient and well-structured codebase.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!