TNG / ArchUnit

A Java architecture test library, to specify and assert architecture rules in plain Java
http://archunit.org
Apache License 2.0
3.19k stars 288 forks source link

Check access classes only works for some cases #1311

Open ramon2 opened 4 months ago

ramon2 commented 4 months ago

Hello, I have encountered strange behavior in one of the architecture tests that checks that no class from the 'application' layer can use classes from the 'infrastructure' layer. In some cases, the test fails (desired behavior), but in other cases, the test does not fail (undesired behavior).

I would like to know if this is an error in the library or if I am coding the tests incorrectly. The test is as follows:

package com.adevinta.archtests

import ...

@AnalyzeClasses(
    packages = ["com.adevinta.archtests"],
    importOptions = [ImportOption.DoNotIncludeTests::class]
)
class HexagonalArchitectureTest {
    @ArchTest
    val `the domain classes should not access the application classes` =
        noClasses()
            .that()
            .resideInAPackage("..application..")
            .should()
            .accessClassesThat()
            .resideInAnyPackage("..infrastructure..")
}

Here is a highly simplified example to illustrate the behavior I'm referring to. Case 1: If I print a field from the 'infrastructure' layer class in my 'application' layer class, then the test fails (desired behavior).

package com.adevinta.archtests.application

import com.adevinta.archtests.infrastructure.controller.CarController

class CarUseCase {

    fun execute(carController: CarController) {
        println("CarUseCase: ${carController.name}")
    }
}

Case 2: If I print the class without accessing any of its fields, then the test does not fail (undesired behavior).

package com.adevinta.archtests.application

import com.adevinta.archtests.infrastructure.controller.CarController

class CarUseCase {

    fun execute(carController: CarController) {
        println("CarUseCase: ${carController}")
    }
}

The CarController in this case is a dummy class like this:

package com.adevinta.archtests.infrastructure.controller

data class CarController(
    val name: String
)

However, if I run the following tests using 'layeredArchitecture' (see the following example), then the tests always fail for both cases in the example (desired result). I would like to know if the first test approach is incorrect because we have widely implemented the first solution in all our microservices in my company.

    @ArchTest
    val `layer dependencies between modules are respected` =
        Architectures.layeredArchitecture().consideringAllDependencies()
            .layer("application").definedBy("..application..")
            .layer("domain").definedBy("..domain..")
            .layer("infrastructure").definedBy("..infrastructure..")
            .whereLayer("application").mayOnlyBeAccessedByLayers("infrastructure")
            .whereLayer("domain").mayOnlyBeAccessedByLayers("infrastructure", "application")
            .whereLayer("infrastructure").mayNotBeAccessedByAnyLayer()            
hankem commented 4 months ago

ArchUnit's notion of "access" is more specific than of "dependencies", cf. documentation of [accessClassesThat()](https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/lang/syntax/elements/ClassesShould.html#accessClassesThat()):

'access' refers only to violations by real accesses, i.e. accessing a field, and calling a method. Compare with [dependOnClassesThat()](https://javadoc.io/doc/com.tngtech.archunit/archunit/latest/com/tngtech/archunit/lang/syntax/elements/ClassesShould.html#dependOnClassesThat()) that catches a wider variety of violations.

So you could implement your HexagonalArchitectureTest with

@ArchTest
val `the domain classes should not access the application classes` =
    noClasses().that().resideInAPackage("..application..")
        .should().dependOnClassesThat().resideInAnyPackage("..infrastructure..")

I'll have to double check why the dependency

Method <..application.CarUseCase.execute(..infrastructure.controller.CarController)> has parameter of type <..infrastructure.controller.CarController> in (CarUseCase.kt:0)

is caught by whereLayer("infrastructure").mayNotBeAccessedByAnyLayer().