//Execution environment
* AdoptOpenJDK 11.0.9.1+1
* Spring Boot 2.4.0
* JUnit 5.7.0
* ArchUnit 0.14.1
The basic idea is the same as the onion architecture of ArchUnit Practice: Onion Architecture Architecture Test on the 23rd day.
package com.example.api.employee;
import com.example.application.employee.EmployeeEditInputData;
import com.example.application.employee.EmployeeEditOutputData;
import com.example.application.employee.EmployeeEditUseCase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmployeeController {
private final EmployeeEditUseCase editUseCase;
public EmployeeController(final EmployeeEditUseCase editUseCase) {
this.editUseCase = editUseCase;
}
@PutMapping("/employees/{id}")
public ResponseEntity<EmployeeEditResponse> edit(
@PathVariable final int id,
@RequestBody final EmployeeEditRequest request
) {
EmployeeEditInputData inputData = request.toInputData(id);
EmployeeEditOutputData outputData = editUseCase.execute(inputData);
EmployeeEditResponse response = EmployeeEditResponse.fromOutputData(outputData);
return ResponseEntity.ok(response);
}
}
Added an architecture test to the ArchUnit practice: Onion Architecture architecture test on the 23rd day to express the relationship between the Interface Adapters layer and the Application Business Rules layer (Controller delegates actual processing to UseCase
).
package com.example;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.AccessTarget;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
class ArchitectureTest {
//Class to be inspected
private static final JavaClasses CLASSES =
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("com.example");
@Test
void package dependencies() {
onionArchitecture()
// Enterprise Business Rules
.domainModels("com.example.domain.model..")
.domainServices("com.example.domain.service..")
// Application Business Rules
.applicationServices("com.example.application..")
// Interface Adapters
.adapter("controllers", "com.example.api..")
.adapter("gateways", "com.example.persistence..")
.check(CLASSES);
}
@Test
void Controller delegates actual processing to UseCase() {
classes()
.that()
.resideInAPackage("com.example.api..")
.and().areAnnotatedWith(RestController.class)
.should(new ArchCondition<>("delegate the process to UseCases") {
@Override
public void check(final JavaClass controller, final ConditionEvents events) {
JavaConstructor constructor = controller.getConstructors()
.stream().findFirst().orElseThrow();
boolean isUseCaseInjectedIntoConstructor = constructor.getRawParameterTypes()
.stream().anyMatch(this::isUseCase);
if (! isUseCaseInjectedIntoConstructor) {
//If UseCase is not constructor injected
events.add(SimpleConditionEvent.violated(controller, String.format(
"`%s` does not have the parameter type of UseCase.", constructor.getFullName())));
return;
}
controller.getMethods().stream()
.filter(this::isRequestHandler)
.forEach(handler -> {
boolean doesHandlerExecuteUseCase = handler.getMethodCallsFromSelf()
.stream().anyMatch(this::isUseCase);
if (! doesHandlerExecuteUseCase) {
//If the handler method is not executing UseCase
events.add(SimpleConditionEvent.violated(controller, String.format(
"`%s` does not execute the method of UseCase.", handler.getFullName())));
}
});
}
private boolean isUseCase(final JavaClass clazz) {
return isUseCaseInterface(clazz) && clazz.getMethods().stream().anyMatch(this::isUseCaseMethod);
}
private boolean isUseCase(final JavaMethodCall methodCall) {
return isUseCaseInterface(methodCall.getTargetOwner()) && isUseCaseMethod(methodCall.getTarget());
}
private boolean isUseCaseInterface(final JavaClass clazz) {
return clazz.getPackageName().startsWith("com.example.application")
&& clazz.getSimpleName().endsWith("UseCase")
&& clazz.isInterface();
}
private <T extends HasParameterTypes & HasReturnType> boolean isUseCaseMethod(final T method) {
return method.getRawParameterTypes().size() == 1
&& method.getRawParameterTypes().get(0).getSimpleName().endsWith("InputData")
&& method.getRawReturnType().getSimpleName().endsWith("OutputData");
}
private boolean isRequestHandler(final JavaMethod method) {
return method.isAnnotatedWith(RequestMapping.class)
|| method.isAnnotatedWith(GetMapping.class)
|| method.isAnnotatedWith(PostMapping.class)
|| method.isAnnotatedWith(PutMapping.class)
|| method.isAnnotatedWith(PatchMapping.class)
|| method.isAnnotatedWith(DeleteMapping.class);
}
})
.check(CLASSES);
}
}
An example of a test failure assuming that an architecture violation was detected in which the Controller class relied on its concrete class instead of the UseCase interface.
$ ./gradlew clean check
> Task :test
ArchitectureTest >Controller delegates actual processing to UseCase() FAILED
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package 'com.example.api..' and are annotated with @RestController should delegate the process to UseCases' was violated (1 times):
`com.example.api.employee.EmployeeController.<init>(com.example.application.employee.EmployeeEditInteractor)` does not have the parameter type of UseCase.
at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:82)
at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:198)
at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81)
at com.example.ArchitectureTest.Controller delegates actual processing to UseCase(ArchitectureTest.java:675)
2 tests completed, 1 failed
An example of test failure on the assumption that the handler method of Controller does not execute the method of UseCase (= processing is not delegated to UseCase), which is an architecture violation.
$ ./gradlew clean check
> Task :test
ArchitectureTest >Controller delegates actual processing to UseCase() FAILED
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package 'com.example.api..' and are annotated with @RestController should delegate the process to UseCases' was violated (1 times):
`com.example.api.employee.EmployeeController.edit(int, com.example.api.employee.EmployeeEditRequest)` does not execute the method of UseCase.
at com.tngtech.archunit.lang.ArchRule$Assertions.assertNoViolation(ArchRule.java:94)
at com.tngtech.archunit.lang.ArchRule$Assertions.check(ArchRule.java:82)
at com.tngtech.archunit.lang.ArchRule$Factory$SimpleArchRule.check(ArchRule.java:198)
at com.tngtech.archunit.lang.syntax.ObjectsShouldInternal.check(ObjectsShouldInternal.java:81)
at com.example.ArchitectureTest.Controller delegates actual processing to UseCase(ArchitectureTest.java:673)
2 tests completed, 1 failed