This time, I would like to write about a library called ArchUnit that I was particularly interested in in Technology Radar Vol.19 announced in November 2018. think.
This article shows the result of actually moving the hand while referring to the official information, but if you implement it using ArchUnit, it is not this article, but the official [ArchUnit User Guide](https: //) Please refer to www.archunit.org/userguide/html/000_Index.html). Also, if you find something wrong in the article, please comment.
ArchUnit is a library for architectural testing for applications written in Java / Kotlin. Since ArchUnit itself is OSS, the source code is published on GitHub. https://github.com/TNG/ArchUnit/blob/master/licenses/asm.license
ArchUnit allows you to perform various architectural tests such as dependencies between packages and classes, checking class packaging, checking circular references, and more. ArchUnit is also a library that can easily implement the "fitness function" described in Evolutionary Architecture [Technology Radar]( It is introduced at https://www.thoughtworks.com/radar/tools/archunit). The fitness function is an index for measuring the characteristics of an architecture. The fitness function can be realized by incorporating the test using ArchUnit into CI / CD. For those who want to know more about fitness functions, see Evolutionary Architecture and [Technology Radar Vol.18](https://www.thoughtworks. See com / radar / techniques / architectural-fitness-function).
From here, I would like to introduce the actual test using ArchUnit in the code base. The ArchUnit User Guide is used as a reference, so if you want to know more details, please be sure to refer to this. The operating environment is assumed to be Maven 3.5.4, Spring Boot 2.1.0, Kotlin 1.3, JUnit 4.
For JUnit4 / Maven, you can add the following to pom.xml.
pom.xml
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit4</artifactId>
<version>0.9.3</version>
<scope>test</scope>
</dependency>
When actually creating a test class, load ArchUnitRunner using the @RunWith annotation as shown below. Also, specify the base package of the application to be tested with the @AnalyzeClasses annotation, and add @ArchTest
to the test method.
Sample.kt
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.dais39.sample.app"])
class TestApplicationRules {
@ArchTest
fun test(classes: JavaClasses) {
//Test process
}
}
If you want to test package dependencies, write code similar to the following:
In ArchUnit, as a basic test flow, rule
is described in the method chain, and finally the test is performed by callingcheck ()
. A feature of ArchUnit is that you can write expressive code by connecting methods.
The example below guarantees that all classes that reference classes placed in the application package are placed in the presentation or application package. The test will fail if referenced by another package.
Sample.kt
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.dais39.sample.app"])
class TestApplicationRules {
@ArchTest
Classes in the fun application layer are independent of classes other than the presentation layer(classes: JavaClasses) {
val rules = ArchRuleDefinition.classes().that().resideInAPackage("..application..")
.should().onlyBeAccessed().byClassesThat().resideInAPackage("..presentation..")
rules.check(classes)
}
}
Here's a test if you want to ensure that the Service class is only accessed by the Controller class: You can use haveNameMatching ()
to do pattern matching with a regular expression, but you can use haveSimpleName ()
to specify the class name directly. In addition to this, there are methods for performing various pattern matching.
Sample.kt
@ArchTest
The fun Service class is accessed only from the Controller class(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().haveNameMatching(".*Service")
.should().onlyBeAccessed().byClassesThat().haveNameMatching(".*Controller")
rules.check(classes)
}
A test that guarantees that a class is located in a particular package is written as follows:
Sample.kt
@ArchTest
Classes with names starting with fun ToDoService are placed in the application package(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().haveSimpleNameStartingWith("ToDoService")
.should().resideInAPackage("..application..")
rules.check(classes)
}
A test that guarantees that an interface is implemented only from a class with certain conditions is written as follows. In the example below, the condition is a class that ends with the string specified using haveSimpleNameEndingWith ()
.
Sample.kt
@ArchTest
fun ToDoServiceImpl implements the ToDoService interface(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().implement(ToDoService::class.java)
.should().haveSimpleNameEndingWith("ToDoServiceImpl")
rules.check(classes)
}
Below is a test that ensures that the ToDoService class is only accessible from the class with @ RestController
.
Sample.kt
@ArchTest
The fun ToDoService class is only accessed by the class with the RestController annotation(classes: JavaClasses){
val rules = ArchRuleDefinition.classes().that().areAssignableTo(ToDoService::class.java)
.should().onlyBeAccessed().byClassesThat().areAnnotatedWith(RestController::class.java)
rules.check(classes)
}
ArchUnit allows you to write tests that guarantee a package structure that conforms to the layer architecture. Currently, it supports only layer architecture, but it seems that it will support hexagonal architecture and clean architecture in the future. Below are the tests that guarantee an architecture that applies DIP (Dependency Reversal Principle) to the layer architecture.
Sample.kt
@ArchTest
The structure of the fun application follows the layer architecture(classes: JavaClasses){
val rules = Architectures.layeredArchitecture()
.layer("Presentation").definedBy("..presentation..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Infrastructure").definedBy("..infra..")
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Infrastructure")
.whereLayer("Infrastructure").mayNotBeAccessedByAnyLayer()
rules.check(classes)
}
If you want to create a test that checks for the existence of circular references in your application, write: Specify the target package with matching ()
.
Sample.kt
@ArchTest
fun Circular reference does not exist(classes: JavaClasses){
val rules = SlicesRuleDefinition.slices().matching("com.dais39.sample.app.(*)..").should().beFreeOfCycles()
rules.check(classes)
}
What is ArchUnit this time? What can you do with ArchUnit? I introduced that. It's a library I personally highly recommend for being able to test the architecture and supporting Kotlin, so I hope more people will be interested in this article.