ArchUnit testing

ArchUnit is a test library that allows us to verify that an application adheres to a given set of architectural rules or conventions of Java projects.

Why do we need to test our architecture?

The three main software quality goals to adhere to are maintainability, replace-ability, and extensibility. A great way to ensure these quality goals, is to ensure coding, structure and configuration consistency. Below are the reasons why consistency can change:

  • Projects will often have developers joining or leaving with each having different coding styles.
  • Projects tend to grow with lots of new features and codes.
  • Adding new libraries.
  • Using a lot of design patterns.

Why use ArchUnit?

ArchUnit library allows us to do the following checks:

  • Package dependency checks.
  • Class dependency checks.
  • Class and package containment checks.
  • Inheritance checks.
  • Annotation checks.
  • Layer checks.
  • Cycle checks.

ArchUnit setup.

ArchUnit integrates nicely with the JUnit test framework.

<!-- Junit 4 -->
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit4</artifactId>
    <version>0.15.0</version>
    <scope>test</scope>
</dependency>
<!-- Junit 5 -->
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>0.15.0</version>
    <scope>test</scope>
</dependency>

Examples:

Archunit offers a lot of different type of tests. Here we will cover some of the most common architectural features.

  • Coding convention tests(Field injection, interface locations, logger instantiations) .
  • Naming convention tests(Classes names with respect to their packages)
  • Annotation convention tests(Annotations used in classes with respect to their packages)
  • Layers convention test(classes dependencies)
Source code

All codes are available at: https://github.com/hramdin28/spring-archunit-test

Project structure

1. Coding conventions test
@ArchTag("CodingConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
        ImportOption.DoNotIncludeTests.class,     // Do not scan Test classes
        ImportOption.DoNotIncludeJars.class,      // Do not scan Jars
        ImportOption.DoNotIncludeArchives.class}) // Do not use Archives
public class CodingConventionTest {
    /**
     * Check field injection is not used in classes
     */
    @ArchTest
    private final ArchRule no_field_injection = NO_CLASSES_SHOULD_USE_FIELD_INJECTION;

    /**
     * Check interfaces are not available in packages ..impl..
     */
    @ArchTest
    static final ArchRule interfaces_must_not_be_placed_in_implementation_packages =
            noClasses()
                    .that().resideInAPackage("..impl..")
                    .should().beInterfaces();

    /**
     * Check loggers are private, static, final
     */
    @ArchTest
    private final ArchRule loggers_should_be_private_static_final =
            fields()
                    .that()
                    .haveRawType(Logger.class)
                    .should().bePrivate()
                    .andShould().beStatic()
                    .andShould().beFinal();
}
2. Naming Convention Test
@ArchTag("NamingConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
        ImportOption.DoNotIncludeTests.class,
        ImportOption.DoNotIncludeJars.class,
        ImportOption.DoNotIncludeArchives.class})
class NamingConventionTest {

    /**
     * Check classes/interfaces names in packages ..repository ends with Repository
     */
    @ArchTest
    static ArchRule repositories_should_end_with_Repository =
            classes()
                    .that().resideInAPackage("..repository")
                    .should().haveSimpleNameEndingWith("Repository");
    /**
     * Check classes/interfaces names in packages ..service ends with Service
     */
    @ArchTest
    static ArchRule services_should_end_with_Service =
            classes()
                    .that().areInterfaces()
                    .and().resideInAPackage("..service")
                    .should().haveSimpleNameEndingWith("Service");
    /**
     * Check classes/interfaces names in packages ..service.impl ends with ServiceImpl
     */
    @ArchTest
    static ArchRule services_impl_should_end_with_ServiceImpl =
            classes()
                    .that().resideInAPackage("..service.impl")
                    .should().haveSimpleNameEndingWith("ServiceImpl");
    /**
     * Check classes names in packages ..controller ends with Controller
     */
    @ArchTest
    static ArchRule controllers_should_end_with_Controller =
            classes()
                    .that().resideInAPackage("..controller")
                    .should().haveSimpleNameEndingWith("Controller");

}
3. Annotations convention test
@ArchTag("StereotypeConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
        ImportOption.DoNotIncludeTests.class,
        ImportOption.DoNotIncludeJars.class,
        ImportOption.DoNotIncludeArchives.class})
public class AnnotationConventionTest {

    /**
     * Check classes/Interfaces in package ..repository has annotation @Repository
     */
    @ArchTest
    static ArchRule repository_classes_reside_in_repository_package =
            classes()
                    .that().areAnnotatedWith(Repository.class)
                    .should().resideInAPackage("..repository");
    /**
     * Check classes in package ..service.impl has annotation @Service
     */
    @ArchTest
    static ArchRule service_classes_reside_in_service_impl_package =
            classes()
                    .that().areAnnotatedWith(Service.class)
                    .should().resideInAPackage("..service.impl");
    /**
     * Check classes in package ..controller has annotation @RestController
     */
    @ArchTest
    static ArchRule controller_classes_reside_in_controller_package =
            classes()
                    .that().areAnnotatedWith(RestController.class)
                    .should().resideInAPackage("..controller");
    /**
     * Check classes in package ..model has annotation @Entity
     */
    @ArchTest
    static ArchRule entity_classes_reside_in_model_package =
            classes()
                    .that().areAnnotatedWith(Entity.class)
                    .should().resideInAPackage("..model");
}
4. Layers convention test
@ArchTag("LayerConventionTest")
@AnalyzeClasses(packages = "com.hanish", importOptions = {
        ImportOption.DoNotIncludeTests.class,
        ImportOption.DoNotIncludeJars.class,
        ImportOption.DoNotIncludeArchives.class})
public class LayerConventionTest {
    /**
     * Layers testing:
     * Check Controllers not accessed by other classes
     * Check Services are only accessed by Controllers and Main application class
     * Check Repositories are only accessed by services
     */
    @ArchTest
    static final ArchRule layer_dependencies_are_respected = layeredArchitecture()

            .layer("Controllers").definedBy("com.hanish.controller..")
            .layer("Services").definedBy("com.hanish.service..")
            .layer("Repositories").definedBy("com.hanish.repository..")
            .layer("Application").definedBy("com.hanish..")

            .whereLayer("Controllers").mayNotBeAccessedByAnyLayer()
            .whereLayer("Services").mayOnlyBeAccessedByLayers("Controllers", "Application")
            .whereLayer("Repositories").mayOnlyBeAccessedByLayers("Services");
}

References

Leave a Reply