maartenjan.dev

Hi! I'm Maarten-Jan, a Software Engineer focused on Java, Kotlin, Event Sourcing, Kubernetes and of course Bash! I sometimes write down what I do.

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!