feat(test): configure archunit testing

This commit is contained in:
RouxAntoine 2024-10-05 10:09:58 +02:00
parent 8398c5ada8
commit 05d01e0f86
Signed by: antoine
GPG Key ID: 098FB66FC0475E70
8 changed files with 164 additions and 14 deletions

11
pom.xml
View File

@ -26,6 +26,7 @@
<jib-maven-plugin.version>3.4.3</jib-maven-plugin.version>
<micrometer-registry-prometheus.version>1.13.4</micrometer-registry-prometheus.version>
<archunit-junit5.version>1.3.0</archunit-junit5.version>
</properties>
<dependencyManagement>
@ -35,6 +36,11 @@
<artifactId>micrometer-registry-prometheus</artifactId>
<version>${micrometer-registry-prometheus.version}</version>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit-junit5.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@ -58,6 +64,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -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 {

View File

@ -1,10 +1,10 @@
package tk.antoine.roux.domain.model;
public final class Either<T> {
private final Exception error;
public final class Either<E extends Exception, T> {
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<T> {
return success;
}
public static <T> Either<T> right(T success) {
public static <E extends Exception, T> Either<E, T> right(T success) {
return new Either<>(null, success);
}
public static <T> Either<T> left(Exception error) {
public static <E extends Exception, T> Either<E, T> left(E error) {
return new Either<>(error, null);
}

View File

@ -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 <T> ResponseEntity<T> eitherToResponseEntity(Either<T> either, String errorMessage) {
static <E extends Exception, T> ResponseEntity<T> eitherToResponseEntity(Either<E, T> either, String errorMessage) {
ResponseEntity<T> 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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
junit.displayName.replaceUnderscoresBySpaces=true