Cool things one can do with Arch Unit
ArchUnit is a pretty neat library that allows you to test your application architecture. I wanted to share some cool ArchTests we built in our project that may be of use to others.
Enforce limited clas visibility
We are trying to keep concerns separated in our application, taking a Modulith-like approach (without going 'all the
way' with something like Spring Modulith. An ArchUnittest helps us to set
the default visibility of classes to internal or private.
val classes: JavaClasses = ClassFileImporter().importPackages("<your package>")
@Test
fun `classes should not be public unless specified otherwise`() {
classes()
.that()
.haveSimpleNameNotEndingWith("Test")
.and()
.resideOutsideOfPackage("..shared..")
.should(notBePublic())
.check(classes)
}
private fun notBePublic() = object : ArchCondition<JavaClass>("be internal") {
override fun check(clazz: JavaClass, events: ConditionEvents) {
if (clazz.reflect().kotlin.visibility != KVisibility.PUBLIC) {
events.add(
SimpleConditionEvent.satisfied(
clazz,
"${clazz.description} is not public ${clazz.sourceCodeLocation}",
),
)
} else {
events.add(
SimpleConditionEvent.violated(
clazz,
"${clazz.description} is public ${clazz.sourceCodeLocation}",
),
)
}
}
}
Unit tests should sit in the same package as the class under test
We're still in the design phase of our project, where packages and files get moved around a lot. Oftentimes one forgets
to move the corresponding test of a class as well. That's why we made an ArchUnit test to enforce this rule. Doing this
made us recognize that we had tests that are not strictly unit tests, but rather integration tests hitting multiple
classes. We excluded these from this check by naming these *ComponentTest.
val classes: JavaClasses = ClassFileImporter().importPackages("<your package>")
@Test
fun `unit tests should have the same name and package as the class under test`() {
everyUnitTestHasACorrespondingClass().check(classes)
}
fun everyUnitTestHasACorrespondingClass(): ArchRule = ArchRuleDefinition
.classes()
.that()
.haveSimpleNameEndingWith("Test")
.and()
.haveSimpleNameNotContaining("ComponentTest")
.and()
.haveSimpleNameNotContaining("Abstract")
.should(
object : ArchCondition<JavaClass>("have a corresponding class in the same package") {
override fun check(testClass: JavaClass, events: ConditionEvents) {
val testClassName = testClass.simpleName
val mainClassName = testClassName.replaceFirst("Test$".toRegex(), "")
val prodClasses =
testClass
.getPackage()
.classes
.stream()
.filter { c: JavaClass? -> c!!.simpleName == mainClassName }
.collect(Collectors.toSet())
if (prodClasses.isEmpty()) {
events.add(
SimpleConditionEvent.violated(
testClass,
testClass.name + " has no corresponding class " + mainClassName + " in the same package",
),
)
}
}
},
)
No JUnit Assertions
At some point we made the decision to use AssertJ as our test assertion library. We wrote an ADR to record this decision, and we added an ArchUnit test to enforce it:
val classes: JavaClasses = ClassFileImporter().importPackages("<your package>")
@Test
fun `junit assertions should not be used`() {
classes.forEach { clazz ->
clazz.`should not contain junit assertions`()
}
}
private fun JavaClass.`should not contain junit assertions`() {
val illegalImports = this.directDependenciesFromSelf?.map { it.targetClass.name.toString() }!!
.filter { it.startsWith("org.junit.jupiter.api.Assertions") }
assertThat(
illegalImports,
).withFailMessage { "Imports not ok for ${this.name}: $illegalImports" }.isEmpty()
}
And that's it, for now. If I find some other interesting uses, I will post them here. Happy coding!