Architecture Unit Tests using NetArchTest

Using unit tests to document and enforce architecture policies

I recently discovered the ability to easily write architecture unit tests in a solution using a package called NetArchTest.

Before I get into it let's first cover why I think having architecture unit tests is useful:

  • Documenting the architecture

    Unit tests can serve as a form of living documentation. Engineers can easily understand the set of rules or policies that have been defined as "good" in your company. This makes it significantly easier for new engineers joining the team.

  • Assisting in Refactoring

    Architecture unit tests will ensure that refactoring exercises do not cause any inadvertent changes that might break the architecture rules. This gives engineers confidence to proceed and knowing that there is a level of protection when making changes.

  • Early Detection of Errors

    You don't need to wait for an engineer to point out a violation of architecture rules in a pull request. The failure will be raised as soon as the unit test fails. In the longer term this prevents engineers from propagating violations throughout the solution.

  • Promote your best practices

    These unit tests easily encourage adherence to best practices by providing clear specifications to enforce principles such as separation of concerns, modularity, dependency management and maintainability.

  • Continuous Integration and Delivery

    Unit tests are easily integrated into CI pipelines and ensure that any changes to your codebase are automatically test against the architectural standards. A very good quality gate to prevent violations from proceeding further in the life cycle.

Of course the above does rely on well written architecture tests :)

Using NetArchTest to write architecture unit tests

One of the biggest wins here as that you write these tests just as you would any ordinary unit tests.

So let's explore a simple example of how we can use architecture unit tests to assist us.

In my sample I have a basic solution that has a data layer, service layer and presentation layer:

I have a very basic user repository that returns a list of names:

namespace DataAccess.Repositories
{
    public class UserRepository : IUserRepository
    {
        public List<string> GetUsernames()
        {
            return new List<string>
            {
                "John",
                "Doe"
            };
        }
    }
}

My user service has a dependency on the user repository and returns the username list:

using DataAccess.Repositories;

namespace Services.Users
{
    public class UserService : IUserService
    {
        private readonly IUserRepository _userRepository;

        public UserService(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        public List<string> GetUsernames()
        {
            return _userRepository.GetUsernames();
        }
    }
}

In the presentation layer we have a simple ASP.NET Core API. In our architecture policy we prefer not to have our APIs access repositories directly and rather prefer our Controllers to interact with our service layer.

BUT... You don't have architecture tests and we have a new joiner on the team. Our new engineer came from a team that didn't really mind accessing repositories directly in API controllers. So your new engineer produces the following code:

using DataAccess.Repositories;
using Microsoft.AspNetCore.Mvc;

namespace ArchitectureAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly IUserRepository _userRepository;

        public UsersController(IUserRepository userRepository)
        {
            _userRepository = userRepository;
        }

        [HttpGet]
        public IActionResult Get()
        {
            var usernames = _userRepository.GetUsernames();
            return this.Ok(usernames);
        }
    }
}

It works right? So what's the problem?

The above has deviated from what we would consider best practice and is inconsistent with the rest of our implementation. Sure it would be caught in a PR review (we hope) but how much time have you now wasted? Your reviewer would need to explain the violation to your Padawan and then our brave Jedi would need to go back and redo this controller.

Step in our hero, architecture unit tests. NetArchTest provides an easy to understand implementation of enforcing rules. In the snippet below we are creating architecture unit tests for our API Layer. As the test states we are enforcing that our API Layer should not have a dependency on our data access layer in the API Project for classes that reside in our Controller namespace.

using System.Reflection;
using FluentAssertions;
using NetArchTest.Rules;

namespace ArchitectureTests
{
    public class APILayerTests
    {
        [Test]
        public void APILayer_Should_Not_Access_Repository_Layer_Directly()
        {
            var result = Types.InAssembly(Assembly.Load("ArchitectureAPI"))
                .That()
                .ResideInNamespace("ArchitectureAPI.Controllers")
                .ShouldNot()
                .HaveDependencyOn("DataAccess")
                .GetResult();

            result.IsSuccessful.Should().Be(true);
        }
    }
}

Running our unit tests would produce a failure since we have violated that rule:

Now let's refactor our controller and ensure that it adheres to our standards by accessing the service layer instead of the data access layer directly:

using Microsoft.AspNetCore.Mvc;
using Services.Users;

namespace ArchitectureAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly IUserService _userService;

        public UsersController(IUserService userService)
        {
            _userService = userService;
        }

        [HttpGet]
        public IActionResult Get()
        {
            var usernames = _userService.GetUsernames();
            return this.Ok(usernames);
        }
    }
}

After the refactor we have a happy satisfied test:

By having architecture unit tests in place it would have been much easier for our new team member to understand our rules or have picked up the violation before even raising the PR.

NetArchTest has many other filters such as:

  • AreAbstract

  • AreClasses

  • AreInterfaces

  • AreGeneric

  • ArePublic

  • AreSealed and more....

This makes it easy to target various components that you would like to test against. Some other useful tests using NetArchTest could be:

  • Ensuring certain classes are sealed

  • Following a naming convention

  • Ensuring certain classes are internal

  • Ensuring necessary dependencies and more...

NetArchTest also offers the abilities to create policies which are a collection of rules (Something to explore).

I personally plan on exploring architecture tests further with my teams to see how it would help us. While it's not a "catch all" solution it certainly does greatly assist with setting standards and adhering to them.

Some important considerations for me are:

  • Have a clear plan of how you would maintain these tests going forward.

  • Have the ability to adjust your tests in phases for refactors.

  • Ensure that you have the buy in of the entire team to support the initiative.

  • Sell the idea as a safety net rather than an "enforcer".

You can find my sample application here.

This article was inspired by https://www.milanjovanovic.tech/blog/enforcing-software-architecture-with-architecture-tests