All Downloads are FREE. Search and download functionalities are using the official Maven repository.

generator.server.javatool.archunit.test.HexagonalArchTest.mustache Maven / Gradle / Ivy

There is a newer version: 1.22.0
Show newest version
package {{packageName}};

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static java.util.function.Predicate.*;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.library.Architectures;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

@UnitTest
class HexagonalArchTest {

  private static final String ROOT_PACKAGE = "{{packageName}}";
  private static final String GENERATION_SHARED_KERNEL_PACKAGES = ROOT_PACKAGE.concat(".shared.generation..");
  private static final String WIRE_PACKAGES = ROOT_PACKAGE.concat(".wire..");

  private static final JavaClasses classes = new ClassFileImporter()
    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
    .importPackages(ROOT_PACKAGE);

  private static final Collection businessContexts = packagesWithAnnotation(BusinessContext.class);
  private static final Collection businessContextsPackages = buildPackagesPatterns(businessContexts);

  private static final Collection sharedKernels = packagesWithAnnotation(SharedKernel.class);
  private static final Collection sharedKernelsPackages = buildPackagesPatterns(sharedKernels);

  // the empty package is related to: https://github.com/TNG/ArchUnit/issues/191#issuecomment-507964792
  private static final Collection vanillaPackages = List.of("java..", "");
  private static final Collection commonToolsAndUtilsPackages = List.of("org.apache.commons..", "org.jmolecules..", "org.slf4j..");

  private static Collection buildPackagesPatterns(Collection packages) {
    return packages.stream().map(path -> path + "..").toList();
  }

  private static Collection packagesWithAnnotation(Class annotationClass) throws AssertionError {
    try {
      return Files.walk(rootPackagePath())
        .filter(path -> path.toString().endsWith("package-info.java"))
        .map(toPackageName())
        .map(path -> path.replaceAll("[\\/]", "."))
        .map(path -> path.replaceAll("[\\\\]", "."))
        .map(path -> path.replace("src.main.java.", ""))
        .map(toPackage())
        .filter(pack -> pack.getAnnotation(annotationClass) != null)
        .map(Package::getName)
        .toList();
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  private static Path rootPackagePath() {
    return Stream.of(ROOT_PACKAGE.split("\\.")).map(Paths::get).reduce(Paths.get("src", "main", "java"), Path::resolve);
  }

  private static Function toPackageName() {
    return path -> {
      String stringPath = path.toString();
      return stringPath.substring(0, stringPath.lastIndexOf("."));
    };
  }

  private static Function toPackage() {
    return path -> {
      try {
        return Class.forName(path).getPackage();
      } catch (ClassNotFoundException e) {
        throw new AssertionError();
      }
    };
  }

  @Nested
  class BoundedContexts {

    @Test
    void shouldNotDependOnOtherBoundedContextDomains() {
      Stream.concat(businessContexts.stream(), sharedKernels.stream()).forEach(context -> {
        noClasses()
          .that()
          .resideInAnyPackage(context + "..")
          .should()
          .dependOnClassesThat()
          .resideInAnyPackage(otherBusinessContextsDomains(context))
          .because("Contexts can only depend on classes in the same context or shared kernels")
          .check(classes);
      });
    }

    @Test
    void shouldBeAnHexagonalArchitecture() {
      Stream.concat(businessContexts.stream(), sharedKernels.stream()).forEach(context ->
        Architectures.layeredArchitecture()
          .consideringOnlyDependenciesInAnyPackage(context + "..")
          .withOptionalLayers(true)
          .layer("domain models")
          .definedBy(context + ".domain..")
          .layer("domain services")
          .definedBy(context + ".domain..")
          .layer("application services")
          .definedBy(context + ".application..")
          .layer("primary adapters")
          .definedBy(context + ".infrastructure.primary..")
          .layer("secondary adapters")
          .definedBy(context + ".infrastructure.secondary..")
          .whereLayer("application services")
          .mayOnlyBeAccessedByLayers("primary adapters")
          .whereLayer("primary adapters")
          .mayNotBeAccessedByAnyLayer()
          .whereLayer("secondary adapters")
          .mayNotBeAccessedByAnyLayer()
          .because("Each bounded context should implement an hexagonal architecture")
          .check(classes)
      );
    }

    @Test
    void primaryJavaAdaptersShouldOnlyBeCalledFromSecondaries() {
      classes()
        .that()
        .resideInAPackage("..primary..")
        .and()
        .areMetaAnnotatedWith(Component.class)
        .and()
        .haveSimpleNameStartingWith("Java")
        .should()
        .onlyHaveDependentClassesThat()
        .resideInAPackage("..secondary..")
        .because(
          "To interact between two contexts, secondary from context 'A' should call a primary Java adapter (naming convention starting with 'Java') from context 'B'"
        )
        .check(classes);
    }

    private String[] otherBusinessContextsDomains(String context) {
      return businessContexts.stream().filter(other -> !context.equals(other)).map(name -> name + ".domain..").toArray(String[]::new);
    }
  }

  @Nested
  class Domain {

    @Test
    void shouldNotDependOnOutside() {
      classes()
        .that()
        .resideInAPackage("..domain..")
        .should()
        .onlyDependOnClassesThat()
        .resideInAnyPackage(authorizedDomainPackages())
        .because("Domain model should only depend on domains and a very limited set of external dependencies")
        .check(classes);
    }

    private String[] authorizedDomainPackages() {
      return Stream.of(List.of("..domain.."), vanillaPackages, commonToolsAndUtilsPackages, sharedKernelsPackages)
        .flatMap(Collection::stream)
        .toArray(String[]::new);
    }
  }

  @Nested
  class Application {

    @Test
    void shouldNotDependOnInfrastructure() {
      noClasses()
        .that()
        .resideInAPackage("..application..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..infrastructure..")
        .because("Application should only depend on domain, not on infrastructure")
        .check(classes);
    }
  }

  @Nested
  class Primary {

    @Test
    void shouldNotDependOnSecondary() {
      noClasses()
        .that()
        .resideInAPackage("..primary..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..secondary..")
        .because("Primary should not interact with secondary")
        .check(classes);
    }

    @Test
    void shouldNotHavePublicControllers() {
      noClasses().that().areMetaAnnotatedWith(Controller.class).should().bePublic().check(classes);
    }
  }

  @Nested
  class Secondary {

    @Test
    void shouldNotDependOnApplication() {
      noClasses()
        .that()
        .resideInAPackage("..infrastructure.secondary..")
        .should()
        .dependOnClassesThat()
        .resideInAPackage("..application..")
        .because("Secondary should not depend on application")
        .check(classes);
    }

    @Test
    void shouldNotDependOnSameContextPrimary() {
      Stream.concat(businessContexts.stream(), sharedKernels.stream()).forEach(context -> {
        noClasses()
          .that()
          .resideInAPackage(context + ".infrastructure.secondary..")
          .should()
          .dependOnClassesThat()
          .resideInAPackage(context + ".infrastructure.primary")
          .because("Secondary should not loop to its own context's primary")
          .check(classes);
      });
    }
  }

  @Nested
  class SharedKernels {

    @Test
    void sharedPackageShouldOnlyContainSharedKernels() {
      classes()
        .that()
        .haveSimpleName("package-info")
        .and()
        .resideInAPackage(ROOT_PACKAGE.concat(".shared.."))
        .should()
        .beMetaAnnotatedWith(SharedKernel.class)
        .because(ROOT_PACKAGE + ".shared package should only contain shared kernels")
        .check(classes);
    }
  }

  @Nested
  class Wire {

    @Test
    void shouldNotDependOnBoundedContextsOrSharedKernels() {
      noClasses()
        .that()
        .resideInAPackage(WIRE_PACKAGES)
        .should()
        .dependOnClassesThat()
        .resideInAnyPackage(businessContextsOrSharedKernelsPackages())
        .because("Wire should not depend on business contexts or shared kernel should not depend")
        .check(classes);
    }

    @Test
    void boundedContextsAndSharedKernelsShouldNotDependOnWire() {
      noClasses()
        .that()
        .resideInAnyPackage(businessContextsOrSharedKernelsPackages())
        .should()
        .dependOnClassesThat()
        .resideInAPackage(WIRE_PACKAGES)
        .because("Business contexts and shared kernel should not depend on wire")
        .check(classes);
    }

    private static String[] businessContextsOrSharedKernelsPackages() {
      return Stream.of(businessContextsPackages, sharedKernelsPackages)
        .flatMap(Collection::stream)
        .filter(not(GENERATION_SHARED_KERNEL_PACKAGES::equals))
        .toArray(String[]::new);
    }

    @Test
    void shouldNotHavePublicClasses() {
      noClasses().that().resideInAnyPackage(WIRE_PACKAGES).should().bePublic().check(classes);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy