From c126c349720617b9d24f9c4487b181fefea7a3e8 Mon Sep 17 00:00:00 2001 From: ronsei Date: Thu, 18 Nov 2021 16:02:28 +0100 Subject: [PATCH 1/2] First set tests for Assignment 8 --- tapas-tasks/pom.xml | 19 +++++ .../service/AddNewTaskToTaskListService.java | 3 + .../RetrieveTaskFromTaskListService.java | 2 + .../src/main/resources/application.properties | 35 ++++++++- .../AddNewTaskToTaskListSystemTest.java | 76 ++++++++++++++++++ .../TapasTasksApplicationTests.java | 7 +- ...AddNewTaskToTaskListWebControllerTest.java | 68 ++++++++++++++++ .../mongodb/TaskPersistenceAdapterTest.java | 77 +++++++++++++++++++ .../AddNewTaskToTaskListServiceTest.java | 63 +++++++++++++++ .../tapastasks/tasks/domain/TaskListTest.java | 49 ++++++++++++ .../resources/application-test.properties | 2 + 11 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/AddNewTaskToTaskListSystemTest.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebControllerTest.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskPersistenceAdapterTest.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListServiceTest.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/domain/TaskListTest.java create mode 100644 tapas-tasks/src/test/resources/application-test.properties diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml index 7dcf6ae..6dcc158 100644 --- a/tapas-tasks/pom.xml +++ b/tapas-tasks/pom.xml @@ -37,6 +37,16 @@ spring-boot-starter-data-mongodb + + org.springframework.boot + spring-boot-starter-actuator + + + de.codecentric + chaos-monkey-spring-boot + 2.5.4 + + org.projectlombok lombok @@ -47,6 +57,11 @@ spring-boot-starter-test test + + org.mockito + mockito-core + 2.21.0 + org.springframework.boot @@ -74,6 +89,10 @@ org.eclipse.paho.client.mqttv3 1.2.0 + + org.springframework.boot + spring-boot-test + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java index d78de83..9776064 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java @@ -11,11 +11,14 @@ import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent; import ch.unisg.tapastasks.tasks.domain.TaskList; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + import javax.transaction.Transactional; @RequiredArgsConstructor @Component @Transactional +@Service("AddNewTaskToTaskList") public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase { private final NewTaskAddedEventPort newTaskAddedEventPort; diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java index 5e5ec29..39026f5 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java @@ -7,6 +7,7 @@ import ch.unisg.tapastasks.tasks.domain.Task; import ch.unisg.tapastasks.tasks.domain.TaskList; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.Optional; @@ -14,6 +15,7 @@ import java.util.Optional; @RequiredArgsConstructor @Component @Transactional +@Service("RetrieveTaskFromTaskList") public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase { private final LoadTaskPort loadTaskFromRepositoryPort; diff --git a/tapas-tasks/src/main/resources/application.properties b/tapas-tasks/src/main/resources/application.properties index badc0f8..7588f89 100644 --- a/tapas-tasks/src/main/resources/application.properties +++ b/tapas-tasks/src/main/resources/application.properties @@ -1,7 +1,38 @@ server.port=8081 -//spring.data.mongodb.uri=mongodb://127.0.0.1:27017 -spring.data.mongodb.uri=mongodb://root:8nP7s0a@mongodb:27017/ +spring.data.mongodb.uri=mongodb://127.0.0.1:27017 +#spring.data.mongodb.uri=mongodb://root:8nP7s0a@mongodb:27017/ spring.data.mongodb.database=tapas-tasks baseuri=https://tapas-tasks.86-119-34-23.nip.io/ +spring.profiles.active=chaos-monkey +chaos.monkey.enabled=true +management.endpoint.chaosmonkey.enabled=true +management.endpoint.chaosmonkeyjmx.enabled=true +# include specific endpoints +management.endpoints.web.exposure.include=health,info,chaosmonkey +chaos.monkey.watcher.controller=true +chaos.monkey.watcher.restController=true +chaos.monkey.watcher.service=true +chaos.monkey.watcher.repository=true +chaos.monkey.watcher.component=true +#Chaos Monkey configs taken from here: https://www.baeldung.com/spring-boot-chaos-monkey + +#Latency Assault +#chaos.monkey.assaults.latencyActive=true +#chaos.monkey.assaults.latencyRangeStart=3000 +#chaos.monkey.assaults.latencyRangeEnd=15000 + +#Exception Assault +#chaos.monkey.assaults.latencyActive=false +#chaos.monkey.assaults.exceptionsActive=true +#chaos.monkey.assaults.killApplicationActive=false + +#AppKiller Assault +#chaos.monkey.assaults.latencyActive=false +#chaos.monkey.assaults.exceptionsActive=false +#chaos.monkey.assaults.killApplicationActive=true + +#Chaos Monkey assaults via REST to endpoint /actuator/chaosmonkey/assaults/ +#https://softwarehut.com/blog/tech/chaos-monkey +#https://codecentric.github.io/chaos-monkey-spring-boot/latest/ diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/AddNewTaskToTaskListSystemTest.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/AddNewTaskToTaskListSystemTest.java new file mode 100644 index 0000000..e17c2e2 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/AddNewTaskToTaskListSystemTest.java @@ -0,0 +1,76 @@ +package ch.unisg.tapastasks; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; +import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.*; + +import static org.assertj.core.api.BDDAssertions.*; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class AddNewTaskToTaskListSystemTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private AddTaskPort addTaskPort; + + @Test + void addNewTaskToTaskList() throws JSONException { + + Task.TaskName taskName = new Task.TaskName("system-integration-test-task"); + Task.TaskType taskType = new Task.TaskType("system-integration-test-type"); + Task.OriginalTaskUri originalTaskUri = new Task.OriginalTaskUri("example.org"); + + ResponseEntity response = whenAddNewTaskToEmptyList(taskName, taskType, originalTaskUri); + + JSONObject responseJson = new JSONObject(response.getBody().toString()); + String respTaskId = responseJson.getString("taskId"); + String respTaskName = responseJson.getString("taskName"); + String respTaskType = responseJson.getString("taskType"); + + then(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + then(respTaskId).isNotEmpty(); + then(respTaskName).isEqualTo(taskName.getValue()); + then(respTaskType).isEqualTo(taskType.getValue()); + then(TaskList.getTapasTaskList().getListOfTasks().getValue()).hasSize(1); + + } + + private ResponseEntity whenAddNewTaskToEmptyList( + Task.TaskName taskName, + Task.TaskType taskType, + Task.OriginalTaskUri originalTaskUri) throws JSONException { + + TaskList.getTapasTaskList().getListOfTasks().getValue().clear(); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", TaskJsonRepresentation.MEDIA_TYPE); + + String jsonPayLoad = new JSONObject() + .put("taskName", taskName.getValue() ) + .put("taskType", taskType.getValue()) + .put("originalTaskUri",originalTaskUri.getValue()) + .toString(); + + HttpEntity request = new HttpEntity<>(jsonPayLoad,headers); + + return restTemplate.exchange( + "/tasks/", + HttpMethod.POST, + request, + Object.class + ); + + } + +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java index b96cb8b..a343151 100644 --- a/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java @@ -1,13 +1,18 @@ package ch.unisg.tapastasks; +import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort; +import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort; +import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock; +import ch.unisg.tapastasks.tasks.application.service.AddNewTaskToTaskListService; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class TapasTasksApplicationTests { - @Test + @Test void contextLoads() { + } } diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebControllerTest.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebControllerTest.java new file mode 100644 index 0000000..d397908 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebControllerTest.java @@ -0,0 +1,68 @@ +package ch.unisg.tapastasks.tasks.adapter.in.web; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; +import ch.unisg.tapastasks.tasks.adapter.in.web.AddNewTaskToTaskListWebController; +import ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb.TaskRepository; +import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; +import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase; +import ch.unisg.tapastasks.tasks.domain.Task; +import org.json.JSONObject; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.then; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(controllers = AddNewTaskToTaskListWebController.class) +public class AddNewTaskToTaskListWebControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase; + + @MockBean + TaskRepository taskRepository; + + @Disabled + @Test + void testAddNewTaskToTaskList() throws Exception { + + String taskName = "test-request"; + String taskType = "test-request-type"; + String originalTaskUri = "example.org"; + + String jsonPayLoad = new JSONObject() + .put("taskName", taskName ) + .put("taskType", taskType) + .put("originalTaskUri",originalTaskUri) + .toString(); + + //This raises a NullPointerException since it tries to build the HTTP response with attributes from + //the domain object (created task), which is mocked --> we need System Tests here! + //See the buckpal example from the lecture for a working integration test for testing the web controller + mockMvc.perform(post("/tasks/") + .contentType(TaskJsonRepresentation.MEDIA_TYPE) + .content(jsonPayLoad)) + .andExpect(status().isCreated()); + + then(addNewTaskToTaskListUseCase).should() + .addNewTaskToTaskList(eq(new AddNewTaskToTaskListCommand( + new Task.TaskName(taskName), new Task.TaskType(taskType), + Optional.of(new Task.OriginalTaskUri(originalTaskUri)) + ))); + + } + + +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskPersistenceAdapterTest.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskPersistenceAdapterTest.java new file mode 100644 index 0000000..ef1fa99 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/adapter/out/persistence/mongodb/TaskPersistenceAdapterTest.java @@ -0,0 +1,77 @@ +package ch.unisg.tapastasks.tasks.adapter.out.persistence.mongodb; + +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.mongo.AutoConfigureDataMongo; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@AutoConfigureDataMongo +@Import({TaskPersistenceAdapter.class, TaskMapper.class}) +public class TaskPersistenceAdapterTest { + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private TaskPersistenceAdapter adapterUnderTest; + + @Test + void addsNewTask() { + + String testTaskId = UUID.randomUUID().toString(); + String testTaskName = "adds-persistence-task-name"; + String testTaskType = "adds-persistence-task-type"; + String testTaskOuri = "adds-persistence-test-task-ouri"; + String testTaskStatus = Task.Status.OPEN.toString(); + String testTaskListName = "tapas-tasks-tutors"; + + + Task testTask = new Task( + new Task.TaskId(testTaskId), + new Task.TaskName(testTaskName), + new Task.TaskType(testTaskType), + new Task.OriginalTaskUri(testTaskOuri), + new Task.TaskStatus(Task.Status.valueOf(testTaskStatus)) + ); + adapterUnderTest.addTask(testTask); + + MongoTaskDocument retrievedDoc = taskRepository.findByTaskId(testTaskId,testTaskListName); + + assertThat(retrievedDoc.taskId).isEqualTo(testTaskId); + assertThat(retrievedDoc.taskName).isEqualTo(testTaskName); + assertThat(retrievedDoc.taskListName).isEqualTo(testTaskListName); + + } + + @Test + void retrievesTask() { + + String testTaskId = UUID.randomUUID().toString(); + String testTaskName = "reads-persistence-task-name"; + String testTaskType = "reads-persistence-task-type"; + String testTaskOuri = "reads-persistence-test-task-ouri"; + String testTaskStatus = Task.Status.OPEN.toString(); + String testTaskListName = "tapas-tasks-tutors"; + + MongoTaskDocument mongoTask = new MongoTaskDocument(testTaskId, testTaskName, testTaskType, testTaskOuri, + testTaskStatus, testTaskListName); + taskRepository.insert(mongoTask); + + Task retrievedTask = adapterUnderTest.loadTask(new Task.TaskId(testTaskId), + new TaskList.TaskListName(testTaskListName)); + + assertThat(retrievedTask.getTaskName().getValue()).isEqualTo(testTaskName); + assertThat(retrievedTask.getTaskId().getValue()).isEqualTo(testTaskId); + assertThat(retrievedTask.getTaskStatus().getValue()).isEqualTo(Task.Status.valueOf(testTaskStatus)); + + } + +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListServiceTest.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListServiceTest.java new file mode 100644 index 0000000..10d21fd --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListServiceTest.java @@ -0,0 +1,63 @@ +package ch.unisg.tapastasks.tasks.application.service; + +import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; +import ch.unisg.tapastasks.tasks.application.port.out.AddTaskPort; +import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort; +import ch.unisg.tapastasks.tasks.application.port.out.TaskListLock; +import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Optional; + +import static org.mockito.BDDMockito.*; +import static org.assertj.core.api.Assertions.*; + + +public class AddNewTaskToTaskListServiceTest { + + private final AddTaskPort addTaskPort = Mockito.mock(AddTaskPort.class); + private final TaskListLock taskListLock = Mockito.mock(TaskListLock.class); + private final NewTaskAddedEventPort newTaskAddedEventPort = Mockito.mock(NewTaskAddedEventPort.class); + private final AddNewTaskToTaskListService addNewTaskToTaskListService = new AddNewTaskToTaskListService( + newTaskAddedEventPort, addTaskPort, taskListLock); + + @Test + void addingSucceeds() { + + Task newTask = givenATaskWithNameAndTypeAndURI(new Task.TaskName("test-task"), + new Task.TaskType("test-type"), Optional.of(new Task.OriginalTaskUri("example.org"))); + + TaskList taskList = givenAnEmptyTaskList(TaskList.getTapasTaskList()); + + AddNewTaskToTaskListCommand addNewTaskToTaskListCommand = new AddNewTaskToTaskListCommand(newTask.getTaskName(), + newTask.getTaskType(), Optional.ofNullable(newTask.getOriginalTaskUri())); + + Task addedTask = addNewTaskToTaskListService.addNewTaskToTaskList(addNewTaskToTaskListCommand); + + assertThat(addedTask).isNotNull(); + assertThat(taskList.getListOfTasks().getValue()).hasSize(1); + + then(taskListLock).should().lockTaskList(eq(TaskList.getTapasTaskList().getTaskListName())); + then(newTaskAddedEventPort).should(times(1)) + .publishNewTaskAddedEvent(any(NewTaskAddedEvent.class)); + + } + + private TaskList givenAnEmptyTaskList(TaskList taskList) { + taskList.getListOfTasks().getValue().clear(); + return taskList; + } + + private Task givenATaskWithNameAndTypeAndURI(Task.TaskName taskName, Task.TaskType taskType, + Optional originalTaskUri) { + Task task = Mockito.mock(Task.class); + given(task.getTaskName()).willReturn(taskName); + given(task.getTaskType()).willReturn(taskType); + given(task.getOriginalTaskUri()).willReturn(originalTaskUri.get()); + return task; + } + +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/domain/TaskListTest.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/domain/TaskListTest.java new file mode 100644 index 0000000..05dc664 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/tasks/domain/TaskListTest.java @@ -0,0 +1,49 @@ +package ch.unisg.tapastasks.tasks.domain; + +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; + + +public class TaskListTest { + + @Test + void addNewTaskToTaskListSuccess() { + TaskList taskList = TaskList.getTapasTaskList(); + taskList.getListOfTasks().getValue().clear(); + Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task"), + new Task.TaskType("My-Test-Type")); + + assertThat(newTask.getTaskName().getValue()).isEqualTo("My-Test-Task"); + assertThat(taskList.getListOfTasks().getValue()).hasSize(1); + assertThat(taskList.getListOfTasks().getValue().get(0)).isEqualTo(newTask); + + } + + @Test + void retrieveTaskSuccess() { + TaskList taskList = TaskList.getTapasTaskList(); + Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task2"), + new Task.TaskType("My-Test-Type2")); + + Task retrievedTask = taskList.retrieveTaskById(newTask.getTaskId()).get(); + + assertThat(retrievedTask).isEqualTo(newTask); + + } + + @Test + void retrieveTaskFailure() { + TaskList taskList = TaskList.getTapasTaskList(); + Task newTask = taskList.addNewTaskWithNameAndType(new Task.TaskName("My-Test-Task3"), + new Task.TaskType("My-Test-Type3")); + + Task.TaskId fakeId = new Task.TaskId("fake-id"); + + Optional retrievedTask = taskList.retrieveTaskById(fakeId); + + assertThat(retrievedTask.isPresent()).isFalse(); + } +} diff --git a/tapas-tasks/src/test/resources/application-test.properties b/tapas-tasks/src/test/resources/application-test.properties new file mode 100644 index 0000000..e45b53d --- /dev/null +++ b/tapas-tasks/src/test/resources/application-test.properties @@ -0,0 +1,2 @@ +spring.data.mongodb.uri=mongodb://127.0.0.1:27017 +spring.data.mongodb.database=tapas-tasks From f6c0495c047023a010097ec6f0a6559526766c72 Mon Sep 17 00:00:00 2001 From: ronsei Date: Thu, 18 Nov 2021 18:13:49 +0100 Subject: [PATCH 2/2] ArchUnit test for Assignment 8 --- tapas-tasks/pom.xml | 6 ++ .../unisg/tapastasks/DependencyRuleTests.java | 24 ++++++ .../unisg/tapastasks/archunit/Adapters.java | 62 ++++++++++++++++ .../tapastasks/archunit/ApplicationLayer.java | 59 +++++++++++++++ .../archunit/ArchitectureElement.java | 74 +++++++++++++++++++ .../archunit/HexagonalArchitecture.java | 61 +++++++++++++++ 6 files changed, 286 insertions(+) create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java create mode 100644 tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml index 6dcc158..25ef1f2 100644 --- a/tapas-tasks/pom.xml +++ b/tapas-tasks/pom.xml @@ -62,6 +62,12 @@ mockito-core 2.21.0 + + com.tngtech.archunit + archunit-junit4 + 0.22.0 + test + org.springframework.boot diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java new file mode 100644 index 0000000..0d26f37 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/DependencyRuleTests.java @@ -0,0 +1,24 @@ +package ch.unisg.tapastasks; + +import ch.unisg.tapastasks.archunit.HexagonalArchitecture; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +class DependencyRuleTests { + + @Test + void testPackageDependencies() { + noClasses() + .that() + .resideInAPackage("ch.unisg.tapastasks.tasks.domain..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("ch.unisg.tapastasks.tasks.application..") + .check(new ClassFileImporter() + .importPackages("ch.unisg.tapastasks.tasks..")); + } + +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java new file mode 100644 index 0000000..92a764b --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/Adapters.java @@ -0,0 +1,62 @@ +package ch.unisg.tapastasks.archunit; + +import com.tngtech.archunit.core.domain.JavaClasses; + +import java.util.ArrayList; +import java.util.List; + +public class Adapters extends ArchitectureElement { + + private final HexagonalArchitecture parentContext; + private List incomingAdapterPackages = new ArrayList<>(); + private List outgoingAdapterPackages = new ArrayList<>(); + + Adapters(HexagonalArchitecture parentContext, String basePackage) { + super(basePackage); + this.parentContext = parentContext; + } + + public Adapters outgoing(String packageName) { + this.incomingAdapterPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public Adapters incoming(String packageName) { + this.outgoingAdapterPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + List allAdapterPackages() { + List allAdapters = new ArrayList<>(); + allAdapters.addAll(incomingAdapterPackages); + allAdapters.addAll(outgoingAdapterPackages); + return allAdapters; + } + + public HexagonalArchitecture and() { + return parentContext; + } + + String getBasePackage() { + return basePackage; + } + + void dontDependOnEachOther(JavaClasses classes) { + List allAdapters = allAdapterPackages(); + for (String adapter1 : allAdapters) { + for (String adapter2 : allAdapters) { + if (!adapter1.equals(adapter2)) { + denyDependency(adapter1, adapter2, classes); + } + } + } + } + + void doesNotDependOn(String packageName, JavaClasses classes) { + denyDependency(this.basePackage, packageName, classes); + } + + void doesNotContainEmptyPackages() { + denyEmptyPackages(allAdapterPackages()); + } +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java new file mode 100644 index 0000000..e8e00a5 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ApplicationLayer.java @@ -0,0 +1,59 @@ +package ch.unisg.tapastasks.archunit; + +import com.tngtech.archunit.core.domain.JavaClasses; + +import java.util.ArrayList; +import java.util.List; + +public class ApplicationLayer extends ArchitectureElement { + + private final HexagonalArchitecture parentContext; + private List incomingPortsPackages = new ArrayList<>(); + private List outgoingPortsPackages = new ArrayList<>(); + private List servicePackages = new ArrayList<>(); + + public ApplicationLayer(String basePackage, HexagonalArchitecture parentContext) { + super(basePackage); + this.parentContext = parentContext; + } + + public ApplicationLayer incomingPorts(String packageName) { + this.incomingPortsPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public ApplicationLayer outgoingPorts(String packageName) { + this.outgoingPortsPackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public ApplicationLayer services(String packageName) { + this.servicePackages.add(fullQualifiedPackage(packageName)); + return this; + } + + public HexagonalArchitecture and() { + return parentContext; + } + + public void doesNotDependOn(String packageName, JavaClasses classes) { + denyDependency(this.basePackage, packageName, classes); + } + + public void incomingAndOutgoingPortsDoNotDependOnEachOther(JavaClasses classes) { + denyAnyDependency(this.incomingPortsPackages, this.outgoingPortsPackages, classes); + denyAnyDependency(this.outgoingPortsPackages, this.incomingPortsPackages, classes); + } + + private List allPackages() { + List allPackages = new ArrayList<>(); + allPackages.addAll(incomingPortsPackages); + allPackages.addAll(outgoingPortsPackages); + allPackages.addAll(servicePackages); + return allPackages; + } + + void doesNotContainEmptyPackages() { + denyEmptyPackages(allPackages()); + } +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java new file mode 100644 index 0000000..465f441 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/ArchitectureElement.java @@ -0,0 +1,74 @@ +package ch.unisg.tapastasks.archunit; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; + +import java.util.List; + +import static com.tngtech.archunit.base.DescribedPredicate.*; +import static com.tngtech.archunit.base.DescribedPredicate.greaterThanOrEqualTo; +import static com.tngtech.archunit.lang.conditions.ArchConditions.*; +import static com.tngtech.archunit.lang.conditions.ArchConditions.containNumberOfElements; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +abstract class ArchitectureElement { + + final String basePackage; + + public ArchitectureElement(String basePackage) { + this.basePackage = basePackage; + } + + String fullQualifiedPackage(String relativePackage) { + return this.basePackage + "." + relativePackage; + } + + static void denyDependency(String fromPackageName, String toPackageName, JavaClasses classes) { + noClasses() + .that() + .resideInAPackage("ch.unisg.tapastasks.tasks.domain..") + .should() + .dependOnClassesThat() + .resideInAnyPackage("ch.unisg.tapastasks.tasks.application..") + .check(classes); + } + + static void denyAnyDependency( + List fromPackages, List toPackages, JavaClasses classes) { + for (String fromPackage : fromPackages) { + for (String toPackage : toPackages) { + noClasses() + .that() + .resideInAPackage(matchAllClassesInPackage(fromPackage)) + .should() + .dependOnClassesThat() + .resideInAnyPackage(matchAllClassesInPackage(toPackage)) + .check(classes); + } + } + } + + static String matchAllClassesInPackage(String packageName) { + return packageName + ".."; + } + + void denyEmptyPackage(String packageName) { + classes() + .that() + .resideInAPackage(matchAllClassesInPackage(packageName)) + .should(containNumberOfElements(greaterThanOrEqualTo(1))) + .check(classesInPackage(packageName)); + } + + private JavaClasses classesInPackage(String packageName) { + return new ClassFileImporter().importPackages(packageName); + } + + void denyEmptyPackages(List packages) { + for (String packageName : packages) { + denyEmptyPackage(packageName); + } + } +} diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java new file mode 100644 index 0000000..1445968 --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/archunit/HexagonalArchitecture.java @@ -0,0 +1,61 @@ +package ch.unisg.tapastasks.archunit; + +import com.tngtech.archunit.core.domain.JavaClasses; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class HexagonalArchitecture extends ArchitectureElement { + + private Adapters adapters; + private ApplicationLayer applicationLayer; + private String configurationPackage; + private List domainPackages = new ArrayList<>(); + + public static HexagonalArchitecture boundedContext(String basePackage) { + return new HexagonalArchitecture(basePackage); + } + + public HexagonalArchitecture(String basePackage) { + super(basePackage); + } + + public Adapters withAdaptersLayer(String adaptersPackage) { + this.adapters = new Adapters(this, fullQualifiedPackage(adaptersPackage)); + return this.adapters; + } + + public HexagonalArchitecture withDomainLayer(String domainPackage) { + this.domainPackages.add(fullQualifiedPackage(domainPackage)); + return this; + } + + public ApplicationLayer withApplicationLayer(String applicationPackage) { + this.applicationLayer = new ApplicationLayer(fullQualifiedPackage(applicationPackage), this); + return this.applicationLayer; + } + + public HexagonalArchitecture withConfiguration(String packageName) { + this.configurationPackage = fullQualifiedPackage(packageName); + return this; + } + + private void domainDoesNotDependOnOtherPackages(JavaClasses classes) { + denyAnyDependency( + this.domainPackages, Collections.singletonList(adapters.basePackage), classes); + denyAnyDependency( + this.domainPackages, Collections.singletonList(applicationLayer.basePackage), classes); + } + + public void check(JavaClasses classes) { + this.adapters.doesNotContainEmptyPackages(); + this.adapters.dontDependOnEachOther(classes); + this.adapters.doesNotDependOn(this.configurationPackage, classes); + this.applicationLayer.doesNotContainEmptyPackages(); + this.applicationLayer.doesNotDependOn(this.adapters.getBasePackage(), classes); + this.applicationLayer.doesNotDependOn(this.configurationPackage, classes); + this.applicationLayer.incomingAndOutgoingPortsDoNotDependOnEachOther(classes); + this.domainDoesNotDependOnOtherPackages(classes); + } +}