Architecture tests with ArchUnit, pt. 1: Reasons | | https://mobileit.cz/Blog/Pages/arch-unit-1.aspx | Architecture tests with ArchUnit, pt. 1: Reasons | <p>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.</p><p>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).</p><p>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.</p><p>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, <a href="https://www.archunit.org/">ArchUnit</a>!
</p><h2>What’s all the fuss about?</h2><p>ArchUnit is a library to, well, unit test your architecture. There are other tools to
<em>check</em> your architecture, but the “unit testing” part of ArchUnit is actually its killer
feature.</p><p>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
<em>and</em> 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?</p><p>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:</p>
<img alt="ArchUnit test results in Android Studio" src="/Blog/PublishingImages/Articles/arch-unit-1-01.png" data-themekey="#" />
<p>Suddenly the whole onboarding situation looks much brighter, doesn’t it?</p><h2>Show me the code</h2><p>Enough talk, let’s get down to business! If your test framework of choice is JUnit 4, put this
in your <span class="pre-inline">build.gradle.kts</span>:</p><pre><code class="kotlin hljs">dependencies {
testImplementation("com.tngtech.archunit:archunit-junit4:0.14.1")
}
</code></pre><p>There are artifacts for other test frameworks as well, just refer to the <a href="https://www.archunit.org/userguide/html/000_Index.html#_installation">docs</a>. Be
careful not to use older versions as this version contains important fixes for multi-module
projects containing Android libraries in a CI environment.</p><p>Now we can write our first architecture test:</p><pre><code class="kotlin hljs">@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")
}
</code></pre><p>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:</p><p><span class="pre-inline">@AnalyzeClasses</span> annotation is one of the ways to specify what to
check. Here, we simply want to test all code in
the <span class="pre-inline">com.example.myapp</span> 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. <em>Where</em> ArchUnit actually gets this bytecode is a slightly
more complicated question, but that’s not important right now.</p><p>Anyway, we annotate our test cases with <span class="pre-inline">@ArchTest</span> 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.</p><p>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.</p><h2>Test the tests</h2><p>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?</p><p>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!</p><p>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).</p><p>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.</p><p>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.</p><h2>Be careful what you test for</h2><p>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.</p><p>Basically all simple ArchUnit rules follow the form <span class="pre-inline">(no) LANGUAGE_ELEMENT that PREDICATE should (not) CONDITION</span>.
From a mathematical point of view, these rules are <em>implications</em>.</p><p>An implication looks like this:</p>
<img alt="Venn diagram of an implication" src="/Blog/PublishingImages/Articles/arch-unit-1-02.png" data-themekey="#" />
<p>For our example test above (and many other tests that you’ll write), it means that the test will
pass for <em>all</em> these variants:</p><ul><li>class is not assignable to <span class="pre-inline">ViewModel::class</span> and does not
have a simple name ending with <span class="pre-inline">ViewModel</span> (that’s OK)
</li><li>class is assignable to <span class="pre-inline">ViewModel::class</span> and has a simple
name ending with <span class="pre-inline">ViewModel</span> (that’s also OK)
</li><li>class is not assignable to <span class="pre-inline">ViewModel::class</span> and has a simple
name ending with <span class="pre-inline">ViewModel</span> (the criss-crossed part of the
diagram; we don’t really want to allow this)
</li></ul><p>It seems that what we really want is an equivalence:</p>
<img alt="Venn diagram of an equivalence" src="/Blog/PublishingImages/Articles/arch-unit-1-03.png" data-themekey="#" />
<p>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:</p><pre><code class="kotlin hljs">@ArchTest
val `classes named ViewModel should have correct super class` =
classes().that().haveSimpleNameEndingWith("ViewModel")
.should().beAssignableTo(ViewModel::class.java)
</code></pre><p>This way, the offending case which the first test didn’t catch (class name ends with
<span class="pre-inline">ViewModel</span>, but it is not assignable to
<span class="pre-inline">ViewModel.java</span>) is prevented.</p><h2>Best thing since sliced bread</h2><p>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.</p><p>We’ve only scratched the surface of what’s possible, so <a href="/Blog/Pages/arch-unit-2.aspx">next
time</a>, we’ll dive into ArchUnit APIs to discover some nifty architecture testing goodness!
</p>
| | #architecture;#jvm;#tdd;#android | | |
Architecture tests with ArchUnit, pt. 2: Rules | | https://mobileit.cz/Blog/Pages/arch-unit-2.aspx | Architecture tests with ArchUnit, pt. 2: Rules | <p>In the <a href="/Blog/Pages/arch-unit-1.aspx">first part</a> 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!</p><h2>First things first</h2><p>ArchUnit rules follow the pattern
<span class="pre-inline">(no) LANGUAGE_ELEMENT that PREDICATE should (not) CONDITION</span>.
So what language elements can we use?</p><p>All tests begin with static methods in <span class="pre-inline">ArchRuleDefinition</span> class
(but please import the class to make the rules more readable).</p><p>We can start with <span class="pre-inline">classes</span> or <span class="pre-inline">constructors</span>
which are pretty self-explanatory. We also have <span class="pre-inline">theClass</span> if you
want to be brief and specific. If possible, always use the overload that takes <span class="pre-inline">Class<*></span> 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.</p><p>Next, we have <span class="pre-inline">fields</span>, <span class="pre-inline">methods</span> and
<span class="pre-inline">members</span>. When testing Kotlin code, be extra careful with <span class="pre-inline">fields</span> because Kotlin properties are <em>not</em> Java
fields. Remember that ArchUnit checks compiled bytecode and every Kotlin property is actually
compiled to getter method by prepending the <span class="pre-inline">get</span> prefix, setter
method by prepending the <span class="pre-inline">set</span> prefix (only for <span class="pre-inline">var</span> properties) and private field with the same name as the
property name, but <em>only for properties with backing fields</em>. 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.</p><p>We also have a slightly mysterious <span class="pre-inline">codeUnits</span> method—it means
simply anything that can access other code (including methods, constructors, initializer blocks,
static field assignments etc.).</p><p>All methods mentioned above also have their negated variants. Now what can we do with all
this?</p><h2>Packages, packages everywhere</h2><p>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 <span class="pre-inline">map</span>, <span class="pre-inline">plannedtrips</span>, <span class="pre-inline">routeplanning</span>,
<span class="pre-inline">speedlimits</span>, <span class="pre-inline">tolls</span>, <span class="pre-inline">vehicles</span> or <span class="pre-inline">voiceguidance</span>,
you’ll get a pretty good idea about what the app is really about. But if instead you are looking at
packages such as <span class="pre-inline">activities</span>, <span class="pre-inline">fragments</span>, <span class="pre-inline">services</span>, <span class="pre-inline">di</span>, <span class="pre-inline">data</span>, <span class="pre-inline">apis</span>, etc., it won’t tell you much about the
application (every Android app will contain at least some of those things).</p><p>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):</p><pre><code class="kotlin hljs">@ArchTest
val `every class should reside in one of the specified packages` =
classes().should().resideInAnyPackage(
"..di",
"..ui",
"..presentation",
"..domain",
"..data"
)
</code></pre><p>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.</p><p>This test however doesn’t say anything about the package structure <em>above</em> the leaves, so
if you want to be more strict, you can write this, for example:
</p><pre><code class="kotlin hljs">@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"
)
</code></pre><p>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 <span class="pre-inline">**</span> which
matches any sequence of characters <em>including</em> the dot. Together with the two dot
notation, you can express pretty much any package structure conceivable (see the Javadoc for
<span class="pre-inline">PackageMatcher</span> class).</p><h2>Building the walls</h2><p>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.</p><p>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:</p><pre><code class="kotlin hljs">@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 */
)
</code></pre><p>Only a few lines and those pesky imports have no way of creeping in your pristine (and now even
fairly platform-independent!) code.</p><h2>Piece(s) of cake</h2><p>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.</p><p>Suppose we have defined these layers and their <em>code</em> dependencies:</p>
<img alt="Example layer structure" src="/Blog/PublishingImages/Articles/arch-unit-2-01.png" data-themekey="#" />
<p>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).</p><p>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:</p><pre><code class="kotlin hljs">@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()
</code></pre><p>How does it work? <span class="pre-inline">layeredArchitecture()</span> is a static method in the
<span class="pre-inline">Architectures</span> class (again, please import it). First we need to
actually define our layers: <span class="pre-inline">layer</span> declares the layer (the
argument is simply any descriptive String constant) and <span class="pre-inline">definedBy</span> 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 <span class="pre-inline">withOptionalLayers(true)</span>
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).</p><p>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.</p><h2>Inner beauty</h2><p>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 <span class="pre-inline">UseCase</span> classes and <span class="pre-inline">Repository</span>
interfaces in there. Furthermore, we want for these classes to follow certain name conventions
and to extend correct base classes.</p><p>We can express all these requirements by the following set of ArchUnit tests:</p><pre><code class="kotlin hljs">@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")
</code></pre><p>And as a bonus example for Android fans, you can, of course, be even more specific:</p><pre><code class="kotlin hljs">@ArchTest
val `no one should ever name fields like this anymore ;)` =
noFields().should().haveNameMatching("m[A-Z]+.*")
</code></pre><h2>Endless power</h2><p>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!</p><p>Next time, we’ll take a look at some advanced features
and configuration options.</p> | | #architecture;#jvm;#tdd;#android | | |
Architecture tests with ArchUnit, pt. 3: Advanced stuff | | https://mobileit.cz/Blog/Pages/arch-unit-3.aspx | Architecture tests with ArchUnit, pt. 3: Advanced stuff | <p>ArchUnit has many tricks up to its sleeve. We’ve <a href="/Blog/Pages/arch-unit-2.aspx">already
seen</a> how to check package structure, language elements such as classes, fields, and methods, and
how to make sure your layered architecture is sound. But there’s more! Let’s take a look at some
advanced concepts.</p><h2>Slicing and dicing</h2><p>As we’ve seen in the previous part of this series, ArchUnit makes it easy to test layers
and their relationships. However, dividing your codebase only horizontally isn’t enough—in all
but very tiny projects, you’d end up with huge layers containing too many unrelated classes.
Thus we need to divide our code vertically as well, usually by user-facing features and
project-specific or general-purpose libraries. Those features or libraries are often compilation
units (e.g., Gradle modules), but that doesn’t need to concern us at this moment.</p>
<img alt="Package and module structure" src="/Blog/PublishingImages/Articles/arch-unit-3-01.png" data-themekey="#" />
<p>So, if we have defined several vertical slices of our codebase, we would like to test their
relationships as well. Horizontal layer tests work <em>across</em> all slices, so they won’t
help us in this case, but ArchUnit has us covered with its high-level slices API:</p><pre><code class="kotlin hljs">@ArchTest
val `feature slices should not depend on each other` =
slices().matching("com.example.myapp.feature.(*)..")
.should().notDependOnEachOther()
</code></pre><p>This is a good rule to have, as you usually want your features to be isolated from each other.
How does it work?</p><p>First, we define the matcher which slices the codebase vertically: It takes package notation
which we’ve seen in the previous rules. The matcher group denoted by parentheses specifies the
actual slicing point as well as the slice’s name shown in error messages.</p><p>In this case, code units residing in the following example packages <em>and</em> their
subpackages would constitute separate slices: <span class="pre-inline">com.example.myapp.feature.login</span>,
<span class="pre-inline">com.example.myapp.feature.map</span> or <span class="pre-inline">com.example.myapp.feature.navigation</span>.
</p>
<img alt="Feature slices" src="/Blog/PublishingImages/Articles/arch-unit-3-02.png" data-themekey="#" />
<p>In contrast, writing <span class="pre-inline">matching("com.example.myapp.feature.(**)")</span>, then
<span class="pre-inline">com.example.myapp.feature.login.model</span> and <span class="pre-inline">com.example.myapp.feature.login.data</span> would constitute
<em>different</em> slices.</p>
<img alt="Layer slices" src="/Blog/PublishingImages/Articles/arch-unit-3-03.png" data-themekey="#" />
<p>Be careful, as this distinction might be rather subtle!</p><p>As always when practicing TDD, you need to see your test fail for the right reason—in
this case that means creating a temporary file that intentionally breaks the test and deleting it
afterwards.</p><p>The rest of the rule is simple: After the usual <span class="pre-inline">should()</span>
operator, we have only two options: <span class="pre-inline">notDependOnEachOther()</span> tests
that, well, no slice depends on any other (unlike the layer dependency tests, these tests are
bi-directional), whereas <span class="pre-inline">beFreeOfCycles()</span>
allows dependencies between the slices, but only in one direction at most.</p><p>Generally speaking, it may be a good idea to run the <span class="pre-inline">beFreeOfCycles()</span> test on every slice (using one of the two test
variants mentioned above) in your codebase, whereas some types of slices (typically
libraries, but not features) may be permitted to depend on each other in one direction.
</p><p>But what if your codebase isn’t structured in such a convenient way? For example, there might be
no middle <span class="pre-inline">feature</span> package distinguishing features from
libraries, or worse, the package structure may be completely inconsistent.</p><p>For such cases, ArchUnit contains handy <span class="pre-inline">SliceAssignment</span> interface which
you can use to assign slices to classes in a completely arbitrary way:</p><pre><code class="kotlin hljs">@ArchTest
private val features = object : SliceAssignment {
override fun getIdentifierOf(javaClass: JavaClass) = when {
javaClass.packageName.startsWith("com.example.myapp.login") -> SliceIdentifier.of("feature-login")
javaClass.name.contains("map") -> SliceIdentifier.of("feature-map")
/* ... whatever you need ... */
else -> SliceIdentifier.ignore()
override fun getDescription() = "this will be added to the error message"
}
</code></pre><p>Strings given to <span class="pre-inline">SliceIdentifier</span> are arbitrary constants
identifying the slice and are also shown in error messages.</p><p>There is an important difference in what you write in that <span class="pre-inline">else</span>
branch: If you return <span class="pre-inline">SliceIdentifier.of("remaining")</span>, then all
classes not matching the previous cases will be assigned to the <span class="pre-inline">"remaining"</span>
slice (which means they will be tested against other slices), whereas if you return <span class="pre-inline">SliceIdentifier.ignore()</span>, those classes won’t participate in
the test at all (both options have their uses, but be careful not to confuse them).</p><p>We can then use our slice assignment like this:</p><pre><code class="kotlin hljs">slices().assignedFrom(features).should().notDependOnEachOther()</code></pre><h2>Why be in when you could be out?</h2><p>As we’ve learned, ArchUnit runs its tests on compiled bytecode. But where do these classes come
from?</p><p>There is more than one way to specify that, but probably the most succinct is to use this
annotation:</p><pre><code class="kotlin hljs">@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(packages = ["com.example.myapp"])
internal class MyArchTest
</code></pre><p>Besides using String literals, we can specify packages with Classes or, if that’s not enough,
completely customize the sources using ArchUnit’s <span class="pre-inline">LocationProvider</span>. In every case, please note that ArchUnit
looks for packages within the current classpath and <em>all</em> classes must be imported
for ArchUnit to be able to work correctly—if you import class <span class="pre-inline">X</span>,
you need to import all its dependencies as well, transitively, otherwise ArchUnit will have only
a limited amount of information to work with and the tests might yield false positives, giving you
a false sense of security.</p><p>Now we have all the class locations that ArchUnit needs, but there are situations when we don’t
necessarily need to test against all of the classes in there. We can filter the classes with
<span class="pre-inline">importOptions</span>:</p><pre><code class="kotlin hljs">@RunWith(ArchUnitRunner::class)
@AnalyzeClasses(
packages = ["com.example.myapp"],
importOptions = [
DoNotIncludeArchives::class,
DoNotIncludeTests::class,
DoNotIncludeAndroidGeneratedClasses::class
]
)
internal class MyArchTest
</code></pre><p>ArchUnit comes with a couple of handy predefined import options, such as the first two, or we can
write our own, which is simple enough:</p><pre><code class="kotlin hljs">internal class DoNotIncludeAndroidGeneratedFiles : ImportOption {
companion object {
private val pattern = Pattern.compile(".*/BuildConfig\\.class|.*/R(\\\$.*)?\\.class|.*Binding\\.class")
}
override fun includes(location: Location) = location.matches(pattern)
}
</code></pre><p>This import option rejects Android <span class="pre-inline">BuildConfig</span>, <span class="pre-inline">R</span>, and <span class="pre-inline">Binding</span> classes. The location
argument passed here is platform-independent, so you don’t have to worry about path separators
and such things.</p><p>But what if we need to be more granular? For example, sometimes we might need to ignore certain
classes on a per-test basis, basically adding ad-hoc exceptions to our pristine rules, because,
you know, the real world happens. This is a slippery slope, so don’t forget to document such
situations, but it’s simple enough to do by adding <span class="pre-inline">or</span> clause to
the rule:</p><pre><code class="kotlin hljs">@ArchTest
val `domain layer should contain only specified classes` =
classes().that().resideInAPackage("..domain..")
.should().haveSimpleNameEndingWith("Repository").andShould().beInterfaces()
.orShould().haveSimpleNameEndingWith("Controller").andShould().beInterfaces()
.orShould().haveSimpleNameEndingWith("UseCase")
.orShould().be(Data::class.java)
.because("only repositories, controllers and use cases are permitted in domain and Data is special wrapper for results from those classes")
</code></pre><p>Be specific as possible with your exceptions as you don’t want them to accidentally match more
language elements than intended—here, using class literal instead of String is safe and future
refactoring-proof.</p><p><span class="pre-inline">Because</span> clause allows you to add more detail to the default error
message generated from the test case name. There is also <span class="pre-inline">`as`</span>
clause (ArchUnit being written in Java, it accidentally overloads Kotlin’s keyword, so don’t
forget to escape it or create an alias extension function with a Kotlin-friendly name) that allows you to completely override the error message.</p><h2>Godspeed</h2><p>Because ArchUnit does a lot under the hood (bytecode examination and cycle checks tend to be
expensive, among other things), the tests themselves, although written using unit test
infrastructure, seldom have the <em>speed</em> of normal unit tests. The speed penalty
can be
quite severe, so organize your test suites in a way that ArchUnit tests don’t slow you down when
developing your code using the classic TDD cycle of red-green-refactor, which has to be always
blazingly fast.</p><p>That said, all ArchUnit’s features working together allow us to express our architecture in a
clean, succinct, and <em>executable</em> way, which is invaluable in larger projects. Consistency
and
clarity are virtues that may very well make the difference between a codebase that is easy to
learn and maintain and one that becomes a big steaming pile of unreadable mess.</p> | | #architecture;#jvm;#tdd;#android | | |
Architecture tests with ArchUnit, pt. 4: Extensions & Kotlin support | | https://mobileit.cz/Blog/Pages/arch-unit-4.aspx | Architecture tests with ArchUnit, pt. 4: Extensions & Kotlin support | <p>ArchUnit is <a href="/Blog/Pages/arch-unit-3.aspx">immensely capable on its own</a> and that's a great merit on its own, but it doesn’t stop there—ArchUnit’s power can be augmented by adding
custom matchers, language elements, and even whole new concepts. In this post, we’ll look at how
we can achieve that and then we’ll see if we can leverage these capabilities to support even
Kotlin-exclusive language elements in ArchUnit tests (spoiler alert: yes, we can!). Ready?</p><h2>Shiny new things</h2><p>As we’ve mentioned several times, ArchUnit rules look like this:</p><pre><code class="kotlin hljs">(no) LANGUAGE_ELEMENT that PREDICATE should (not) CONDITION</code></pre><p>As it turns out, by thoroughly following the open/closed principle, ArchUnit allows us to supply our
own language elements, predicates and conditions. These can be simple aggregations of existing
built-in predicates or conditions to facilitate reuse, or we can invent entirely new
domain-specific concepts to utilize in our architecture tests. So how is it done?</p><p>To create a custom language element, predicate or condition, we need to extend
<span class="pre-inline">AbstractClassesTransformer</span>, <span class="pre-inline">DescribedPredicate</span>,
or <span class="pre-inline">ArchCondition</span> respectively. Each abstract base class takes
one type argument—the language element it operates on (ArchUnit provides for example <span class="pre-inline">JavaClass</span>, <span class="pre-inline">JavaMember</span>, <span class="pre-inline">JavaField</span> or <span class="pre-inline">JavaCodeUnit</span> and
we can even create our own; these are reflection-like models read from compiled bytecode). They
also have one constructor argument, a String that is appended to the rule description in error
messages.</p><p>The simplest one to implement is <span class="pre-inline">DescribedPredicate</span>—we need to
override its <span class="pre-inline">apply</span> method which takes the examined language
element and returns Boolean:</p><pre><code class="kotlin hljs">val myPredicate = object : DescribedPredicate("rule description") {
override fun apply(input: JavaClass): Boolean = // …
}</code></pre><p><span class="pre-inline">ArchCondition</span> is slightly more involved, as its <span class="pre-inline">check</span> function takes the language element as well. In addition, it also takes <span class="pre-inline">ConditionEvents</span> collection, which is used to
return the result of the evaluation, as this function doesn’t directly return anything:
</p><pre><code class="kotlin hljs">val myCondition = object : ArchCondition("condition description") {
override fun check(item: JavaClass, events: ConditionEvents) {
if (item.doesNotSatisfyMyCondition()) {
events.add(SimpleConditionEvent.violated(item, "violation description"))
}
}
}</code></pre><p><span class="pre-inline">AbstractClassesTransformer</span> has a <span class="pre-inline">doTransform</span>
method which takes a collection of <span class="pre-inline">JavaClass</span>es and
transforms it to another collection. Elements of the output collection can be <span class="pre-inline">JavaClass</span>es as well, different built-in types or even custom
classes. The transformation may comprise any number of operations including mapping or
filtering:</p><pre><code class="kotlin hljs">val myTransformer = object : AbstractClassesTransformer("items description") {
override fun doTransform(collection: JavaClasses): Iterable =
collection.filter { /* ... */ }.map { /* ... */ }
}</code></pre><p>Anyway, we can use our custom transformers, predicates, and conditions like this:</p><pre><code class="kotlin hljs">all(encryptedRepositories)
.that(handleGdprData)
.should(useSufficientlyStrongCrypthographicAlgorithms)</code></pre><p>and they can, of course, be combined with the built-in ones.</p><p>Besides promoting reuse, custom transformers, predicates, and conditions are particularly good at
increasing the level of abstraction of your tests—it’s better to describe your system using its
domain language instead of low-level, opaque technical terms.</p><h2>Gimme some Kotlin lovin’</h2><p>As promised, now it’s the time to tackle the last thing we’d like to have in ArchUnit
tests—Kotlin support.</p><p>Because ArchUnit reads compiled bytecode and Kotlin has killer Java interoperability, we can get
pretty far out of the box, but we still can’t directly test for Kotlin stuff like sealed and
data classes, objects, typealiases, suspending functions etc. To find out how that could be
possible, we need to take a slight detour first.</p><p>When targeting JVM (or Android), Kotlin compiler outputs JVM bytecode. Adding custom bytecodes
for Kotlin-exclusive constructs is of course out of the question, so the compiler must resort to
clever tricks to convert Kotlin stuff to vanilla JVM bytecode. Now, there’s some wiggle room
(JVM bytecode allows some things that Java as a language doesn’t), but still, to achieve
Kotlin’s stellar level of Java interoperability, the compiler must mostly play by Java’s
rules.</p><p>To achieve that, for example, Kotlin compiler generates getters, setters and backing fields for
properties. It also creates encapsulating classes for top level functions and properties, adds new
functions to existing classes (to support data classes) or adds parameters to existing functions
(this is one of the tricks behind suspending functions).</p><p>As a result, when examining the bytecode alone, those Kotlin concepts effectively disappear. But
for Kotlin compiler to be able to compile Kotlin code against another <em>already compiled</em>
Kotlin code (and to see it as Kotlin code, not generic JVM code), this information must be
preserved somewhere.</p><p>Take for example this simple data class:</p><pre><code class="kotlin hljs">data class Money(val amount: BigDecimal, val currency: Currency)</code></pre><p>IntelliJ Idea/Android Studio lets us see bytecode generated from this Kotlin code, which in turn
can be (in most cases) decompiled to equivalent Java code. If we do that with the <span class="pre-inline">Money</span> class, we’ll see something like this:</p><pre><code class="java hljs">@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000,\n\u0002\u0018\u0002\n\ /* rest omitted */ },
d2 = {"Lcom/example/myapp/Money;", "", "amount", "Ljava/math/BigDecimal;", "currency", "Ljava/util/Currency;", "(Ljava/math/BigDecimal;Ljava/util/Currency;)V", "getAmount", "()Ljava/math/BigDecimal;", "getCurrency", "()Ljava/util/Currency;", "component1", "component2", "copy", "equals", "", "other", "hashCode", "", "toString", "", "app-main"}
)
public final class Money {
@NotNull private final BigDecimal amount;
@NotNull private final Currency currency;
@NotNull public final BigDecimal getAmount() { return this.amount; }
@NotNull public final Currency getCurrency() { return this.currency; }
/* rest omitted */</code></pre><p>Bingo! The Java part is pretty straightforward, but it looks like that strange <span class="pre-inline">@Metadata</span> stuff
might be what we need. Indeed, the documentation for <span class="pre-inline">@Metadata</span>
says that “This annotation is present on any class file produced by the Kotlin compiler and is
read by the compiler and reflection.” Its arguments contain various interesting Kotlin-exclusive
bits and pieces related to the class and because it has runtime retention, it will be stored in
binary files, which means we can read them from our ArchUnit tests! If only we could make sense
of that gibberish inside the annotation…</p><h2>Metadata dissection</h2><p>It turns out that we can! There’s a small official library to do just that.</p><p>First, add JVM metadata library to your build script:</p><pre><code class="kotlin hljs">dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.1.0")
}</code></pre><p>Then, our plan of attack is this:</p><ol><li>The starting point is the input of our custom transformer, predicate, or condition, which in
this case will be ArchUnit’s <span class="pre-inline">JavaClass</span> object.
</li><li>ArchUnit can read annotations on the <span class="pre-inline">JavaClass</span> object, so we
examine if Kotlin’s <span class="pre-inline">@Metadata</span> annotation is present.
</li><li>If it is, we use the <a href="https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/ReadMe.md">kotlinx-metadata</a>
library to read the actual metadata. (KotlinPoet has a <a href="https://square.github.io/kotlinpoet/kotlinpoet_metadata/">higher-level API</a>
based on
kotlinx-metadata, which presumably might be a little bit nicer to use; we’ll just use the
basic API here, as the end result will be the same in either case.)
</li><li>We expose the data in some easily digestible object so we can write simple and readable
assertions about it.
</li></ol><p>To make an already long story short, here is the first piece of the puzzle—transformation from
ArchUnit’s <span class="pre-inline">JavaClass</span> to kotlinx-metadata <span class="pre-inline">KmClass</span> model:</p><pre><code class="kotlin hljs">private fun JavaClass.toKmClass(): KmClass? = this
.takeIf { it.isAnnotatedWith(Metadata::class.java) }
?.getAnnotationOfType(Metadata::class.java)
?.let { metadata ->
KotlinClassMetadata.read(
KotlinClassHeader(
kind = metadata.kind,
metadataVersion = metadata.metadataVersion,
bytecodeVersion = metadata.bytecodeVersion,
data1 = metadata.data1,
data2 = metadata.data2,
extraString = metadata.extraString,
packageName = metadata.packageName,
extraInt = metadata.extraInt
)
)
}
?.let { (it as? KotlinClassMetadata.Class)?.toKmClass() }</code></pre><p>If a given <span class="pre-inline">JavaClass</span> is annotated with <span class="pre-inline">@Metadata</span>,
this extension reads the annotation and
converts it to a <span class="pre-inline">KotlinClassMetadata</span> object (mapping the
annotation attributes to the
corresponding properties of <span class="pre-inline">KotlinClassHeader</span> along the way).
</p><p><span class="pre-inline">KotlinClassMetadata</span> is a sealed class and its subclasses
represent various different kinds of classes generated by the Kotlin compiler. There are a few
of them, but to keep things simple we are interested only in “real” classes
(<span class="pre-inline">KotlinClassMetadata.Class</span>) from which we finally extract the
rich <span class="pre-inline">KmClass</span> model (and return
null in all other cases).</p><p>To make our life easier later, we also add this handy extension:</p><pre><code class="kotlin hljs">private fun JavaClass.isKotlinClassAndSatisfies(predicate: (KmClass) -> Boolean): Boolean =
this.toKmClass()?.let { predicate(it) } == true</code></pre><h2>Grand finale</h2><p>Now we can finally write our transformers, predicates, and conditions. Because they will be all
quite similar, let’s create factory methods for them first:</p><pre><code class="kotlin hljs">fun kotlinClassesTransformer(description: String, predicate: (KmClass) -> Boolean) =
object : AbstractClassesTransformer(description) {
override fun doTransform(collection: JavaClasses): Iterable =
collection.filter { it.isKotlinClassAndSatisfies(predicate) }
}</code></pre><pre><code class="kotlin hljs">fun kotlinDescribedPredicate(description: String, predicate: (KmClass) -> Boolean) =
object : DescribedPredicate(description) {
override fun apply(javaClass: JavaClass) =
javaClass.isKotlinClassAndSatisfies(predicate)
}</code></pre><pre><code class="kotlin hljs">fun kotlinArchCondition(
ruleDescription: String,
violationDescription: String,
predicate: (KmClass) -> Boolean
) = object : ArchCondition(ruleDescription) {
override fun check(javaClass: JavaClass, events: ConditionEvents) {
if (!javaClass.isKotlinClassAndSatisfies(predicate)) {
events.add(SimpleConditionEvent.violated(javaClass, "$javaClass $violationDescription"))
}
}
}</code></pre><p>And now, finally, we have everything ready to write things we can actually use in our ArchUnit
tests—for example:</p><pre><code class="kotlin hljs">val kotlinSealedClasses = kotlinClassesTransformer("Kotlin sealed classes") {
it.sealedSubclasses.isNotEmpty()
}</code></pre><pre><code class="kotlin hljs">val areKotlinDataClasses = kotlinDescribedPredicate("are Kotlin data classes") {
Flag.Class.IS_DATA(it.flags)
}</code></pre><pre><code class="kotlin hljs">val beKotlinObjects = kotlinArchCondition("be Kotlin objects", "is not Kotlin object") {
Flag.Class.IS_OBJECT(it.flags)
}</code></pre><p>The predicate lambdas operate on <span class="pre-inline">KmClass</span> instances. <span class="pre-inline">KmClass</span> is quite a low-level but powerful API
to examine <span class="pre-inline">@Metadata</span> annotation content. <span class="pre-inline">KmClass</span> has direct methods or properties for some
Kotlin constructs, while others can be derived from its flags. Sometimes it takes a little bit
of exploration, but all Kotlin-specific stuff is there. Or, for a higher-level API to do the
same, see <a href="https://square.github.io/kotlinpoet/kotlinpoet_metadata/">KotlinPoet
metadata</a>.</p><p>Now we can write tests such as:</p><pre><code class="kotlin hljs">all(kotlinSealedClasses)
.that(resideInAPackage("..presentation"))
.should(haveSimpleNameEndingWith("State"))</code></pre><pre><code class="kotlin hljs">classes()
.that(resideInAPackage("..model")).and(areKotlinDataClasses)
.should(implement(Entity::class.java))</code></pre><pre><code class="kotlin hljs">classes()
.that().areTopLevelClasses().and().areAssignableTo(UseCase::class.java)
.should(beKotlinObjects)</code></pre><p>So there you have it—support for Kotlin constructs in ArchUnit. The sky’s the limit, now your
codebase can be more robust than ever!
</p><h2>Conclusion</h2><p>Well, this was quite a journey! The benefits of having even a small suite of ArchUnit tests in
your project are immense—they prevent subtle, hard-to-catch bugs, act as the best possible
documentation of your architecture, save you time during code reviews and keep your codebase
clean, consistent, maintainable and healthy. They are easy to write, simple to integrate into
your CI/CD pipeline and extendable even beyond the original language. What’s not to like? Start
writing them now and reap the rewards for years to come!</p> | | #architecture;#jvm;#tdd;#android;#kotlin | | |