Architecture tests with ArchUnit, pt. 2: Rules
9/29/2020
Architecture tests with ArchUnit, pt. 2: Rules

In the first part of this series, we’ve had a glimpse of an architecture test written with the almighty ArchUnit, but of course there’s much more! Although ArchUnit’s API looks like Java Reflection API, it also contains powerful constructs to describe dependencies between code or predefined rules for testing popular architecture styles. Let’s see what we’ve got to play with!

First things first

ArchUnit rules follow the pattern (no) LANGUAGE_ELEMENT that PREDICATE should (not) CONDITION. So what language elements can we use?

All tests begin with static methods in ArchRuleDefinition class (but please import the class to make the rules more readable).

We can start with classes or constructors which are pretty self-explanatory. We also have theClass if you want to be brief and specific. If possible, always use the overload that takes Class<*> argument instead of the overload that takes String to make your tests resistant to future refactorings; the same goes for other methods with these argument choices.

Next, we have fields, methods and members. When testing Kotlin code, be extra careful with fields because Kotlin properties are not Java fields. Remember that ArchUnit checks compiled bytecode and every Kotlin property is actually compiled to getter method by prepending the get prefix, setter method by prepending the set prefix (only for var properties) and private field with the same name as the property name, but only for properties with backing fields. When testing Kotlin properties, it may sometimes be safer to test their generated getters or setters. Anyway, these subtle details show the importance of watching your test fail.

We also have a slightly mysterious codeUnits method—it means simply anything that can access other code (including methods, constructors, initializer blocks, static field assignments etc.).

All methods mentioned above also have their negated variants. Now what can we do with all this?

Packages, packages everywhere

Consistent packaging is one of the most important things to get right in the project. We strongly prefer packaging by features first, then by layers. This concept sometimes goes by the name of “screaming architecture”: For example, when you open an Android project and you see top level packages such as map, plannedtrips, routeplanning, speedlimits, tolls, vehicles or voiceguidance, you’ll get a pretty good idea about what the app is really about. But if instead you are looking at packages such as activities, fragments, services, di, data, apis, etc., it won’t tell you much about the application (every Android app will contain at least some of those things).

ArchUnit can enforce correct package structure, prevent deadly cyclic dependencies and much more. Let’s see a few examples (the actual packages mentioned are not important, use what is convenient for your project):

@ArchTest
val `every class should reside in one of the specified packages` =
    classes().should().resideInAnyPackage(
        "..di",
        "..ui",
        "..presentation",
        "..domain",
        "..data"
)

The two dots mean “any number of packages including zero”, so this test says that every class must exist in one of these predefined leaf packages.

This test however doesn’t say anything about the package structure above the leaves, so if you want to be more strict, you can write this, for example:

@ArchTest
val `every class should reside in one of the specified packages` =
    classes().should().resideInAnyPackage(
        "com.example.myapp.*.di",
        "com.example.myapp.*.ui",
        "com.example.myapp.*.presentation",
        "com.example.myapp.*.domain",
        "com.example.myapp.*.data"
    )

The star matches any sequence of characters excluding the dot (for our sample packaging, in its place there would be a feature name), but you can also use ** which matches any sequence of characters including the dot. Together with the two dot notation, you can express pretty much any package structure conceivable (see the Javadoc for PackageMatcher class).

Building the walls

One popular architectural style is to divide the code into layers with different levels. We can define layer level simply as the code’s distance from inputs/outputs—so things like UI, DB or REST clients are pretty low-level, whereas business logic and models are on the opposite side and the application logic sits somewhere in the middle.

In this case, it’s a good idea to isolate higher-level layers from external dependencies such as platform SDK or other invasive frameworks and libraries, since higher levels should be more stable and independent of the implementation details in lower layers. ArchUnit can help us with that:

@ArchTest
val `higher-level classes should not depend on the framework` =
    noClasses().that().resideInAnyPackage("..presentation..", "..domain..")
        .should().dependOnClassesThat().resideInAnyPackage(
            "android..",
            "androidx..",
            "com.google.android..",
            "com.google.firebase.."
            /* and more */
        )

Only a few lines and those pesky imports have no way of creeping in your pristine (and now even fairly platform-independent!) code.

Piece(s) of cake

Speaking of layers, we should not only handle their dependencies on the 3rd party code, but of course also the direct dependencies between them. Although we can use the constructs mentioned above, ArchUnit has another trick up to its sleeve when it comes to layered architectures.

Suppose we have defined these layers and their code dependencies:

Example layer structure

This is just an example, but let’s say that the domain layer is the most high-level, so it must not depend on anything else; presentation and data layers can depend on stuff from domain, UI can see view models in presentation layer (but view models must not know anything about UI) and DI sees all to be able to inject anything (and ideally, no other layer should see DI layer, because classes should not know anything about how they are injected; alas this is not always technically possible).

Whatever your actual layers are, the most important thing is that all dependencies go in one direction only, from lower level layers to higher level layers (this is the basic idea of Clean architecture). ArchUnit can encode these rules in one succinct test:

@ArchTest
val `layers should have correct dependencies between them` =
    layeredArchitecture().withOptionalLayers(true)
        .layer(DOMAIN).definedBy("..domain")
        .layer(PRESENTATION).definedBy("..presentation")
        .layer(UI).definedBy("..ui")
        .layer(DATA).definedBy("..data")
        .layer(DI).definedBy("..di")
        .whereLayer(DOMAIN).mayOnlyBeAccessedByLayers(DI, PRESENTATION, DATA)
        .whereLayer(PRESENTATION).mayOnlyBeAccessedByLayers(DI, UI)
        .whereLayer(UI).mayOnlyBeAccessedByLayers(DI)
        .whereLayer(DATA).mayOnlyBeAccessedByLayers(DI)
        .whereLayer(DI).mayNotBeAccessedByAnyLayer()

How does it work? layeredArchitecture() is a static method in the Architectures class (again, please import it). First we need to actually define our layers: layer declares the layer (the argument is simply any descriptive String constant) and definedBy specifies a package by which the layer is, well, defined (you can use package notation which we’ve seen before; you can also use a more general predicate). Without withOptionalLayers(true) call, ArchUnit will require that all layers exist, which in a multi-module project might not necessarily be true (some modules might for example contain only domain stuff).

This rather short test will have an enormous impact on your codebase—correctly managed dependencies are what prevents your project from becoming a giant mess of spaghetti code.

Inner beauty

We’ve sorted the layers and packages, but what about their content? Take for example the domain layer: Continuing our rather simplified example, we want only UseCase classes and Repository interfaces in there. Furthermore, we want for these classes to follow certain name conventions and to extend correct base classes.

We can express all these requirements by the following set of ArchUnit tests:

@ArchTest
val `domain layer should contain only specified classes` =
    classes().that().resideInAPackage("..domain..")
        .should().haveSimpleNameEndingWith("UseCase")
        .andShould().beTopLevelClasses()
        .orShould().haveSimpleNameEndingWith("Repository")
        .andShould().beInterfaces()

@ArchTest
val `classes named UseCase should extend correct base class` =
    classes().that().haveSimpleNameEndingWith("UseCase")
        .should().beAssignableTo(UseCase::class.java)

@ArchTest
val `use case subclasses should have correct name` =
    classes().that().areAssignableTo(UseCase::class.java)
        .should().haveSimpleNameEndingWith("UseCase")

And as a bonus example for Android fans, you can, of course, be even more specific:

@ArchTest
val `no one should ever name fields like this anymore ;)` =
    noFields().should().haveNameMatching("m[A-Z]+.*")

Endless power

We’ve seen only a smart part of the ArchUnit API, but there’s almost nothing that ArchUnit tests cannot handle. You can examine all Java constructs and their wildest combinations (but always be aware of Kotlin-Java interoperability details and test your tests), go explore!

Next time, we’ll take a look at some advanced features and configuration options.

Tags

#architecture; #jvm; #tdd; #android

Author

Jiří Hutárek

Versions

ArchUnit 0.14.1
Kotlin 1.3.72