From 05d01e0f86ec0b16f8a63e3a507c7f8d81d0c3e8 Mon Sep 17 00:00:00 2001 From: RouxAntoine Date: Sat, 5 Oct 2024 10:09:58 +0200 Subject: [PATCH] feat(test): configure archunit testing --- pom.xml | 11 +++ src/main/java/tk/antoine/roux/Main.java | 2 + .../tk/antoine/roux/domain/model/Either.java | 10 +-- .../antoine/roux/infrastructure/Manager.java | 21 +++--- .../architecture/CleanArchitectureTest.java | 46 +++++++++++++ .../architecture/CodingGoodPracticeTest.java | 67 +++++++++++++++++++ .../tk/antoine/roux/architecture/Layer.java | 20 ++++++ src/test/resources/archunit.properties | 1 + 8 files changed, 164 insertions(+), 14 deletions(-) create mode 100644 src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java create mode 100644 src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java create mode 100644 src/test/java/tk/antoine/roux/architecture/Layer.java create mode 100644 src/test/resources/archunit.properties diff --git a/pom.xml b/pom.xml index 6bb1772..0994827 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ 3.4.3 1.13.4 + 1.3.0 @@ -35,6 +36,11 @@ micrometer-registry-prometheus ${micrometer-registry-prometheus.version} + + com.tngtech.archunit + archunit-junit5 + ${archunit-junit5.version} + @@ -58,6 +64,11 @@ spring-boot-starter-test test + + com.tngtech.archunit + archunit-junit5 + test + diff --git a/src/main/java/tk/antoine/roux/Main.java b/src/main/java/tk/antoine/roux/Main.java index 7045d2c..e842613 100644 --- a/src/main/java/tk/antoine/roux/Main.java +++ b/src/main/java/tk/antoine/roux/Main.java @@ -5,6 +5,8 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import java.lang.invoke.MethodHandles; + @SpringBootApplication public class Main { diff --git a/src/main/java/tk/antoine/roux/domain/model/Either.java b/src/main/java/tk/antoine/roux/domain/model/Either.java index 20d1272..360f6a5 100644 --- a/src/main/java/tk/antoine/roux/domain/model/Either.java +++ b/src/main/java/tk/antoine/roux/domain/model/Either.java @@ -1,10 +1,10 @@ package tk.antoine.roux.domain.model; -public final class Either { - private final Exception error; +public final class Either { + private final E error; private final T success; - private Either(Exception error, T success) { + private Either(E error, T success) { this.error = error; this.success = success; } @@ -17,11 +17,11 @@ public final class Either { return success; } - public static Either right(T success) { + public static Either right(T success) { return new Either<>(null, success); } - public static Either left(Exception error) { + public static Either left(E error) { return new Either<>(error, null); } diff --git a/src/main/java/tk/antoine/roux/infrastructure/Manager.java b/src/main/java/tk/antoine/roux/infrastructure/Manager.java index 10e74c5..b3338ce 100644 --- a/src/main/java/tk/antoine/roux/infrastructure/Manager.java +++ b/src/main/java/tk/antoine/roux/infrastructure/Manager.java @@ -3,14 +3,23 @@ package tk.antoine.roux.infrastructure; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; +import org.springframework.web.ErrorResponse; import tk.antoine.roux.domain.model.Either; public interface Manager { - static ResponseEntity eitherToResponseEntity(Either either, String errorMessage) { + static ResponseEntity eitherToResponseEntity(Either either, String errorMessage) { ResponseEntity response; - if (either.isLeft()) { - ProblemDetail problemDetail = exceptionToProblemDetail(either, errorMessage); + if (either.isLeft() ) { + + final ProblemDetail problemDetail; + if (either.left() instanceof ErrorResponse exception) { + problemDetail = exception.getBody(); + } else { + problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, either.left().getMessage()); + } + + problemDetail.setTitle(errorMessage); response = ResponseEntity.of(problemDetail).build(); } else { response = ResponseEntity.ok(either.right()); @@ -18,10 +27,4 @@ public interface Manager { return response; } - private static ProblemDetail exceptionToProblemDetail(Either either, String errorMessage) { - ProblemDetail problemDetail = ProblemDetail - .forStatusAndDetail(HttpStatus.BAD_REQUEST, either.left().getMessage()); - problemDetail.setTitle(errorMessage); - return problemDetail; - } } diff --git a/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java b/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java new file mode 100644 index 0000000..15dc2eb --- /dev/null +++ b/src/test/java/tk/antoine/roux/architecture/CleanArchitectureTest.java @@ -0,0 +1,46 @@ +package tk.antoine.roux.architecture; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeArchives; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars; +import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludePackageInfos; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.library.Architectures; + +/** + * Test of clean architecture package dependencies + */ +@AnalyzeClasses(packages = "tk.antoine.roux", importOptions = { + DoNotIncludeArchives.class, + DoNotIncludeJars.class, + DoNotIncludePackageInfos.class +}) +class CleanArchitectureTest { + + @ArchTest + void should_respect_clean_architecture_application_layer(JavaClasses classes) { + Architectures.layeredArchitecture() + .consideringAllDependencies() + + //@formatter:off + .layer(Layer.DOMAIN.name).definedBy(Layer.DOMAIN.path) + .layer(Layer.MODEL.name).definedBy(Layer.MODEL.path) + .optionalLayer(Layer.REPOSITORIES.name).definedBy(Layer.REPOSITORIES.path) + .optionalLayer(Layer.USECASES.name).definedBy(Layer.USECASES.path) + .layer(Layer.INFRASTRUCTURE.name).definedBy(Layer.INFRASTRUCTURE.path) + .optionalLayer(Layer.ADAPTER_IN.name).definedBy(Layer.ADAPTER_IN.path) + .optionalLayer(Layer.ADAPTER_OUT.name).definedBy(Layer.ADAPTER_OUT.path) + //@formatter:on + + .whereLayer(Layer.INFRASTRUCTURE.name).mayNotBeAccessedByAnyLayer() + .whereLayer(Layer.USECASES.name).mayOnlyBeAccessedByLayers(Layer.ADAPTER_IN.name) + .whereLayer(Layer.REPOSITORIES.name).mayOnlyBeAccessedByLayers(Layer.USECASES.name) + .whereLayer(Layer.MODEL.name).mayOnlyBeAccessedByLayers(Layer.INFRASTRUCTURE.name) + .whereLayer(Layer.ADAPTER_OUT.name).mayNotBeAccessedByAnyLayer() + .whereLayer(Layer.ADAPTER_OUT.name).mayNotAccessAnyLayer() + + .check(classes); + } + +} diff --git a/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java b/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java new file mode 100644 index 0000000..56a45ae --- /dev/null +++ b/src/test/java/tk/antoine/roux/architecture/CodingGoodPracticeTest.java @@ -0,0 +1,67 @@ +package tk.antoine.roux.architecture; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.CompositeArchRule; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES; +import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_JARS; +import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_PACKAGE_INFOS; +import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_FIELD_INJECTION; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING; +import static com.tngtech.archunit.library.GeneralCodingRules.NO_CLASSES_SHOULD_USE_JODATIME; + +/** + * Tests of good practice coding usage + */ +class CodingGoodPracticeTest { + private static final String BASE_PACKAGE = "tk.antoine.roux"; + + ClassFileImporter classFileImporter = new ClassFileImporter() + .withImportOption(DO_NOT_INCLUDE_ARCHIVES) + .withImportOption(DO_NOT_INCLUDE_JARS) + .withImportOption(DO_NOT_INCLUDE_PACKAGE_INFOS); + + JavaClasses classes = classFileImporter + .importPackages(BASE_PACKAGE); + + JavaClasses classesWithoutTest = classFileImporter + .withImportOption(DO_NOT_INCLUDE_TESTS) + .importPackages(BASE_PACKAGE); + + @Test + @DisplayName("Should prevent field injection everywhere, except in test") + void preventFieldInjection() { + CompositeArchRule commonUsageRules = CompositeArchRule + .of(NO_CLASSES_SHOULD_USE_FIELD_INJECTION); + commonUsageRules.check(classesWithoutTest); + } + + @Test + @DisplayName("Should prevent usage of system stream like out, err, in") + void preventSystemStream() { + CompositeArchRule commonUsageRules = CompositeArchRule + .of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS); + commonUsageRules.check(classes); + } + + @Test + @DisplayName("Should prevent usage of Java.util.logging classes") + void preventJavaUtilLogging() { + CompositeArchRule commonUsageRules = CompositeArchRule + .of(NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING); + commonUsageRules.check(classes); + } + + @Test + @DisplayName("Should prevent usage of JodaTime library") + void preventJodaTime() { + CompositeArchRule commonUsageRules = CompositeArchRule + .of(NO_CLASSES_SHOULD_USE_JODATIME); + commonUsageRules.check(classes); + } +} diff --git a/src/test/java/tk/antoine/roux/architecture/Layer.java b/src/test/java/tk/antoine/roux/architecture/Layer.java new file mode 100644 index 0000000..7e3d47b --- /dev/null +++ b/src/test/java/tk/antoine/roux/architecture/Layer.java @@ -0,0 +1,20 @@ +package tk.antoine.roux.architecture; + +public enum Layer { + DOMAIN("domain", "tk.antoine.roux.domain.."), + MODEL("model", "tk.antoine.roux.domain.model.."), + REPOSITORIES("repositories", "tk.antoine.roux.domain.repositories.."), + USECASES("usecases", "tk.antoine.roux.domain.usecases.."), + INFRASTRUCTURE("infrastructure", "tk.antoine.roux.infrastructure.."), + ADAPTER_IN("adapter_in", "tk.antoine.roux.infrastructure.in.."), + ADAPTER_OUT("adapter_out", "tk.antoine.roux.infrastructure.out..") + ; + + public final String name; + public final String path; + + Layer(String name, String path) { + this.name = name; + this.path = path; + } +} diff --git a/src/test/resources/archunit.properties b/src/test/resources/archunit.properties new file mode 100644 index 0000000..d93e375 --- /dev/null +++ b/src/test/resources/archunit.properties @@ -0,0 +1 @@ +junit.displayName.replaceUnderscoresBySpaces=true