In this article, we’ll explore how to use the NetArchTest.Rules library to write architecture tests for our .NET applications.
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:
We create several projects representing the different layers of this architectural pattern.
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
.
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.