Good architecture is essential for a codebase to enjoy a long and happy life (the other crucial
ingredient is running automated unit tests, but that’s a different blog post). Nowadays there are many
sensible options, including our favorite for mobile apps, Clean arch, but no matter which one
you choose, you’ll need to document and enforce it somehow.
The former is traditionally accomplished by some form of oral history passed from one developer
to another (sometimes augmented by blurry photos of frantically scribbled whiteboard diagrams),
while the latter is sporadically checked during code reviews (if there’s time—which there
isn’t—and you remember all the rules yourself—which you don’t).
Or maybe you even have a set of gradually outdated wiki pages and fancy UML models created in
some expensive enterprise tool. Or complicated but incomplete rules written for rather arcane
static analysis frameworks. Or any other form of checks and docs, which are usually hard to
change, detached from the actual code and difficult and rather expensive to maintain.
But fret not! There is a new architectural sheriff in this JVM town of ours and he’s going to
take care of all of this—say hello to your new best friend, ArchUnit!
What’s all the fuss about?
ArchUnit is a library to, well, unit test your architecture. There are other tools to
check your architecture, but the “unit testing” part of ArchUnit is actually its killer
feature.
While “normal” unit tests should describe behavior (not structure!) of the system under test,
ArchUnit cleverly leverages JVM and existing unit test frameworks to let you document
and check your architecture in a form of runnable unit tests, executable in your
current unit test environment (because you already have a strong suite of unit tests, right?).
Why exactly is this such a welcome improvement?
Well, it all boils down to the fundamental benefits of all unit tests: Because unit tests are
code, they are a precise, up-to-date, unambiguous, executable specification of the system. Docs
can be outdated and misleading, but unit tests either compile or don’t; they either pass or not.
Imagine opening a project you don’t know anything about, running its unit tests and seeing
this:
Suddenly the whole onboarding situation looks much brighter, doesn’t it?
Show me the code
Enough talk, let’s get down to business! If your test framework of choice is JUnit 4, put this
in your build.gradle.kts:
dependencies {
testImplementation("com.tngtech.archunit:archunit-junit4:0.14.1")
}
There are artifacts for other test frameworks as well, just refer to the docs. Be
careful not to use older versions as this version contains important fixes for multi-module
projects containing Android libraries in a CI environment.
Now we can write our first architecture test:
@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.example.myapp"])
internal class UiLayerTest {
@ArchTest
val `view model subclasses should have correct name` =
classes().that().areAssignableTo(ViewModel::class.java)
.should().haveSimpleNameEndingWith("ViewModel")
}
And just like that, you now have one small naming convention documented and automatically
verified across your whole project. The API does a great job at being self-explanatory and we’ll
get into the details later, but let’s quickly recap what we have here:
@AnalyzeClasses annotation is one of the ways to specify what to
check. Here, we simply want to test all code in
the com.example.myapp package and its subpackages. ArchUnit imports
and checks Java bytecode (not source files), which is why it works with Kotlin (or any other JVM
language), although it’s itself a pure Java library—another example of Kotlin’s stellar
interoperability with Java. Where ArchUnit actually gets this bytecode is a slightly
more complicated question, but that’s not important right now.
Anyway, we annotate our test cases with @ArchTest and for the
shortest syntax, we use properties instead of functions. As with other unit tests, it’s a good
idea to leverage Kotlin’s escaped property names for more readable test outputs.
And then finally for the main course: ArchUnit has a comprehensive, very expressive and really
rather beautiful fluent API for specifying the predicates and their expected outcomes. It’s not
Java reflection and being a pure Java library, ArchUnit doesn’t have constructs for
Kotlin-exclusive language elements, but it’s still more than powerful enough.
Test the tests
Now run the test. Most projects probably stick to this naming convention, so the result bar in
your favorite IDE might be green already. But wait! How do we know that the tests actually
work?
Although they may appear a bit strange, ArchUnit tests are still unit tests and we should treat them as
such. That means we should follow the famous red-green-refactor cycle, albeit modified, because
you absolutely need to see the test fail and it must fail for the correct reason. This is the
only time when you actually test your tests!
What does this mean for ArchUnit tests? The difference from normal TDD for our specific test case
is that we cannot simply write the test first and watch it fail, because if there are no view
models in the project yet, the test will pass. So we need to cheat a little and break the
architecture on purpose, manually, by creating a temporary class violating the naming convention
in the main source set. Then we run the test, watch it fail, delete the class and watch the test
go green (the refactoring part isn’t really applicable here).
This looks like extra work and granted, it can be a bit tedious, but the red part of the cycle
simply cannot be skipped, ever. There is a myriad of logical and technical errors that can
result in the test being wrong or not executed at all and this is your only chance to catch
them. There’s nothing worse than a dead lump of code giving you a false sense of security.
And there’s one more thing to borrow from TDD playbook: Perhaps you are doing a code review or
approving pull request and you discover some construction violating a rule that you haven’t
thought of before. What to do with that? As with all new bugs, don’t rush fixing the bug! The
first thing you should do is write a test exposing the bug (the red part of the cycle)—that
means writing an ArchUnit rule which will fail with the offending code. Only after that, make the
test green. This way, you’ll slowly make your test suite more precise, with the added bonus that
future regressions will be prevented as well.
Be careful what you test for
We’ll take a look at all ArchUnit’s fluent API constructs in a future post, but there’s an
important detail we need to discuss before that.
Basically all simple ArchUnit rules follow the form (no) LANGUAGE_ELEMENT that PREDICATE should (not) CONDITION.
From a mathematical point of view, these rules are implications.
An implication looks like this:
For our example test above (and many other tests that you’ll write), it means that the test will
pass for all these variants:
- class is not assignable to ViewModel::class and does not
have a simple name ending with ViewModel (that’s OK)
- class is assignable to ViewModel::class and has a simple
name ending with ViewModel (that’s also OK)
- class is not assignable to ViewModel::class and has a simple
name ending with ViewModel (the criss-crossed part of the
diagram; we don’t really want to allow this)
It seems that what we really want is an equivalence:
Although ArchUnit doesn’t (yet?) have API elements to specify equivalences, they are fairly
simple to create: Because A ↔ B is the same as (A → B) AND (B → A), we just need to add another
test to our suite:
@ArchTest
val `classes named ViewModel should have correct super class` =
classes().that().haveSimpleNameEndingWith("ViewModel")
.should().beAssignableTo(ViewModel::class.java)
This way, the offending case which the first test didn’t catch (class name ends with
ViewModel, but it is not assignable to
ViewModel.java) is prevented.
Best thing since sliced bread
I don’t want to use the word game-changer, but I just did. Since we started adding ArchUnit tests
to our projects, we have seen significant improvements in developer productivity and the health of our
codebases. Compared to similar solutions, ArchUnit’s simple integration, ease of use and
expressive powers are unmatched.
We’ve only scratched the surface of what’s possible, so next
time, we’ll dive into ArchUnit APIs to discover some nifty architecture testing goodness!