Validate and enforce your software architecture
- 5 minutes read - 866 wordsIn this post I take a look at automated architecture tests, what they are, why you might need them and where they fit in with all the other types of tests.
The test pyramid
Most people are familiar with the test pyramid, in one form or another. First coined by Mike Cohn in his book “Succeeding with Agile”, it looks like this:
Essentially it suggests your test suite should be made up of three groups of tests with each group varying in:
- the number of tests
- the degree of integration vs isolation
- the speed of execution
Now you could argue (and many do) that this diagram is a little simplistic. That’s why I said ”…in one form or another…" above. A simple Google search comes up with many variations:
The issue here is that people are trying to fit all the different types of automated tests into the test pyramid. That isn’t really the takeaway you should take from the pyramid*, but on the plus side, they can offer a simple infographic of the different types of tests.
* Key takeaway of the test pyramid:
Write lots of small and fast unit tests that thoroughly test logic.
Write some coarse-grained tests that test larger concerns or processes.
Write few high-level tests that test your application from end to end.
However, you generally won’t see “architecture tests” on the test pyramid. So let’s define what they are and where they fit…
What are “architecture tests”?
Architecture tests are automated tests that are designed to validate and enforce the architectural design patterns within your codebase. In the same way unit tests automate the manual testing of business logic, architecture tests automate the manual code reviews that would be needed to enforce these design patterns and conventions. And, obviously, do it in a more consistent and less error-prone way.
For example, they can test:
- Architectural style: in a layered architecture you could enforce that layering is adhered to and the correct dependency flow isn’t broken.
- Code naming: perhaps your convention states that all “service” types end with the name
Service
. - Code organisation: perhaps you want all “repository” types reside in a
<module>.Data
project. - Cross-cutting concerns: maybe all “repository” types should derive from a particular base class or implement a particular interface.
- And more…
So where do they fit with other types of tests? Well, they do and they don’t. They are fast running tests that you should run often and get fast feedback from, otherwise you may have gone “off-piste” with the code you are writing. So they are a bit like unit tests. However, they don’t test a single “unit” in isolation, they are doing a form on static analysis of your codebase as a whole. So you can’t really call them a unit test.
Despite all this, the best place for them is alongside your unit tests, due to the frequency that they should be executed.
What do these tests look like?
As ever, there are a number of different open-source packages that help with writing tests of this type. For example:
- ArchUnit: the original Java package, upon which most others are based or inspired by.
- NetArchTest: a fluent API for .NET to enforce architectural rules via unit tests.
- ArchUnitNET: a free, simple library for checking the architecture of C# code.
- ts-arch: a library for checking architecture conventions in TypeScript & JavaScript using any test framework.
- pytestarch: allows users to define architectural rules and test their Python code against them.
So let’s see what the example tests above look like in C# and NetArchTest using their sample library:
Architectural style
Enforce that layering is adhered to and the correct dependency flow isn’t broken:
[Fact]
public void PresentationShouldNotReferenceData()
{
var result = Types.InCurrentDomain()
.That()
.ResideInNamespace("NetArchTest.SampleLibrary.Presentation")
.ShouldNot()
.HaveDependencyOn("NetArchTest.SampleLibrary.Data")
.GetResult();
Assert.True(result.IsSuccessful);
}
Code naming
All “service” types end with the name Service
:
[Fact]
public void ServiceNamesShouldEndWithService()
{
var result = Types.InCurrentDomain()
.That()
.ResideInNamespace("NetArchTest.SampleLibrary.Services")
.Should()
.HaveNameEndingWith("Service")
.GetResult();
Assert.True(result.IsSuccessful);
}
Code organisation
All “repository” types reside in a <module>.Data
project:
[Fact]
public void RepositoriesShouldResideInData()
{
var result = Types.InCurrentDomain()
.That()
.HaveNameEndingWith("Repository")
.Should()
.ResideInNamespaceEndingWith("Data")
.GetResult();
Assert.True(result.IsSuccessful);
}
Cross-cutting concerns
All “repository” types should implement a particular interface:
[Fact]
public void RepositoriesShouldImplementRepositoryInterface()
{
var result = Types.InCurrentDomain()
.That()
.HaveNameEndingWith("Repository")
.And().AreClasses()
.Should()
.ImplementInterface(typeof(IRepository<>))
.GetResult();
Assert.True(result.IsSuccessful);
}
Finding types and asserting conditions
The two key features for all of these libraries are:
- finding and filtering the list of types you want to enforce a rule on
- asserting one or more conditions that these types must satisfy in order to be correct
For NetArchTest this is achieved with two exhaustive lists of built-in
predicates and
conditions.
You can also create your own custom predicates/conditions as well. When combined with its fluent API these provide an expressive and powerful way to define your rules.
Conclusion
In this post I introduced architecture tests and how they can be used to automatically validate and enforce the architectural design patterns within your codebase. I also demonstrated some example tests using C# and the NetArchTest library.
Tests such as these can help keep your codebase clean and consistent with little effort or risk, so I encourage you to give them a try on your current or next project.