2022-02-14
Writing Unit Test to ensure the Integrity of your Software
How to write tests to ensure that decisions made on day one are still valid as your software grows.
These days, you have so many options on how to start a new project that deciding on how to design the software can be an overwhelming problem. Once decisions are made, you create a set of expectations on how certain things will work and how each piece will interact with the other.
As the software grows, decisions made on day one might need to be adapted, improved, or reviewed, and that's what I want to cover here. So how can you ensure that one change won't affect the other decisions made?
In the book "Fundamentals of Software Architecture." An Engineering Approach, they talk a lot about Fitness Functions.
A fitness function is a particular type of objective function that is used to summarise, as a single figure of merit, how close a given design solution is to achieving the set aims. Fitness functions are used in genetic programming and genetic algorithms to guide simulations towards optimal design solutions.
https://en.wikipedia.org/wiki/Fitness_function
The fitness function that this post will cover is the "Architecture fitness function", which they describe as "Any mechanism that provides an objective integrity assessment of some architecture characteristic or combination of architecture characteristics.".
https://www.oreilly.com/library/view/fundamentals-of-software/9781492043447/
A simple example, using the MVC model, where the Controller Layer will not be accessed by any layer, the Service Layer will only be accessed by the Controllers, and the Persistence Layer will only be accessed by the Services.
To do this, I will use the framework called ArchUnit; ArchUnit is a framework available for Java (compatible with Kotlin) and .NET/C# that allows you to check the architecture of your project by analyzing bytecodes and imports of classes against unit tests that are defining the decisions made for your project.
How to start
To show the usage of this framework, I'm going to start a new Spring Boot App implementing a simple MVC model structure.
We need to create the initial project; you can use this link to download the project with the initial setup.
https://start.spring.io/#!type=gradle-project&language=kotlin&platformVersion=2.6.3&packaging=jar&jvmVersion=17&groupId=com.tiarebalbi&artifactId=archunit-demo&name=archunit-demo&description=Writing%20Unit%20Test%20to%20ensure%20the%20Integrity%20of%20your%20Software&packageName=com.tiarebalbi.archunit&dependencies=web,data-jpa,h2
Now with the initial setup, we need to add the ArchUnit dependency:
build.gradle.kts
testImplementation("com.tngtech.archunit:archunit-junit5:0.22.0")
ArchUnit supports different frameworks and versions; for more details, click here.
https://www.archunit.org/userguide/html/000_Index.html#_installation
The goal here is to review the ArchUnit framework. I won't go into details on the implementation side of things but let's take on what the project structure looks like:
src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── tiarebalbi
│ │ └── archunit
│ │ ├── ArchunitDemoApplication.kt
│ │ ├── controllers
│ │ │ └── DemoController.kt
│ │ ├── models
│ │ │ └── Demo.kt
│ │ ├── repositories
│ │ │ └── DemoRepository.kt
│ │ └── services
│ │ └── DemoService.kt
│ └── resources
│ └── application.properties
└── test
└── kotlin
└── com
└── tiarebalbi
└── archunit
└── ArchunitDemoApplicationTests.kt
Looking at this structure, I have some requirements.
To write your initial test, you need two annotations:
The first one is to specify the package you would like to analyze:
@AnalyzeClasses(packages = ["com.tiarebalbi"], importOptions = [ImportOption.DoNotIncludeTests::class])
And the second one in each test:
@ArchTest
fun `A service can only access a repository.`(importedClasses: JavaClasses) {
......
}
Note that in this case, all tests will receive the parameter importedClasses.
In the first test, I want to test the layers of the application to ensure that the access follows the guidelines defined:
@ArchTest
internal fun `A service can only access a repository`(importedClasses: JavaClasses) {
layeredArchitecture()
// Determine the location of all classes in packages containing "controller" in the name
.layer("Controller").definedBy("..controllers..")
.layer("Service").definedBy("..services..")
.layer("Persistence").definedBy("..repositories..")
// Set the expectations
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")
// Runs the validation
.check(importedClasses)
// the other way to write the same test could be:
classes().that().resideInAPackage("..services..")
.should().onlyBeAccessed().byAnyPackage("..controllers..", "..services..")
.check(importedClasses)
}
The tests are as simple as possible; with a powerful DSL expression, you can access all classes and their definitions.
The next step is for you to think about your project and define a set of contractions so then as your software grows, these tests will help detect and understand the side effects of each change.
Check the ArchUnit User Guide and the source code with all examples on GitHub for more details.
https://github.com/tiarebalbi/archunit-demo
https://www.archunit.org/userguide/html/000_Index.html#_layer_checks