Skip to content

Commit

Permalink
ArchUnit, README, Postman, Structure refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
vondacho committed May 6, 2023
1 parent e33acaa commit 6d102e5
Show file tree
Hide file tree
Showing 52 changed files with 600 additions and 170 deletions.
Empty file removed Dockerfile
Empty file.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
# arch-blueprint-java
# arch-blueprint-java

![build workflow](https://github.com/vondacho/arch-blueprint-java/actions/workflows/build.yml/badge.svg)

A Java project as template and pedagogical support for the teaching of Clean Architecture crafting practice.

## Features

CRUD operations on Customer entities exposed by a REST API.

- Web request validation with Atlassian
- Web security based on Basic Authentication
- Application management with Spring Actuator
- Acceptance testing with Cucumber
- Contract testing with Pact and Spring Cloud Contract
- Architecture testing with ArchUnit

## Getting started

- To build the project with `./gradlew clean build`.
- To launch the test suite with `./gradlew clean check`.
- To launch the application with `./gradlew bootRun --args='--spring.profiles.active=test,jpa'`.

## Technical documentation

- Powered by MkDocs
- API documentation powered by Swagger UI
- Architecture documentation powered by Structurizr and AppMap
- [Latest release](https://vondacho.github.io/arch-blueprint-java)
26 changes: 25 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ sourceSets {
compileClasspath += sourceSets.main.get().compileClasspath + sourceSets.test.get().compileClasspath
annotationProcessorPath += sourceSets.test.get().annotationProcessorPath
}
create("archTest") {
java.srcDir("src/archTest/java")
resources.srcDir("src/archTest/resources")
compileClasspath += sourceSets.main.get().compileClasspath + sourceSets.test.get().compileClasspath
annotationProcessorPath += sourceSets.test.get().annotationProcessorPath
}
create("c4") {
java.srcDir("src/c4/java")
resources.srcDir("src/c4/resources")
Expand Down Expand Up @@ -102,7 +108,7 @@ dependencies {
testImplementation("org.hibernate:hibernate-validator:5.2.5.Final")

// ARCH testing
testImplementation("com.tngtech.archunit:archunit:1.0.1")
testImplementation("com.tngtech.archunit:archunit-junit5:1.0.1")

// API documentation
swaggerUI("org.webjars:swagger-ui:4.1.3")
Expand Down Expand Up @@ -181,6 +187,9 @@ gitPublish {
from("build/swagger-ui-apidoc") {
into("${mkdocs.publish.docPath}/api")
}
from("src/doc/postman") {
into("${mkdocs.publish.docPath}/postman")
}
}
}

Expand Down Expand Up @@ -210,13 +219,28 @@ tasks {
events = mutableSetOf(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
}
}
val archTest by registering(Test::class) {
description = "Runs the architecture tests"
group = "verification"
testClassesDirs = sourceSets["archTest"].output.classesDirs
classpath += sourceSets["archTest"].runtimeClasspath
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events = mutableSetOf(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
}
}
named<Copy>("processAcceptanceTestResources") {
duplicatesStrategy = DuplicatesStrategy.WARN
}
named<Copy>("processArchTestResources") {
duplicatesStrategy = DuplicatesStrategy.WARN
}
named<Copy>("processC4Resources") {
duplicatesStrategy = DuplicatesStrategy.WARN
}
check {
dependsOn(acceptanceTest)
dependsOn(archTest)
}
}
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: "3.7"

services:
postgres:
container_name: postgres-blueprint
image: postgres:12-alpine
ports:
- 15432:5432
environment:
- POSTGRES_DB=blueprint
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=blueprint
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package edu.obya.blueprint.customer.at;

import edu.obya.blueprint.ExceptionHandling;
import edu.obya.blueprint.WebSecurityConfiguration;
import edu.obya.blueprint.WebValidationConfiguration;
import edu.obya.blueprint.config.ExceptionHandling;
import edu.obya.blueprint.config.WebSecurityConfiguration;
import edu.obya.blueprint.config.WebValidationConfiguration;
import edu.obya.blueprint.customer.application.CustomerApplicationConfiguration;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.infra.data.jpa.CustomerJpaConfiguration;
import edu.obya.blueprint.customer.web.CustomerController;
import edu.obya.blueprint.customer.web.CustomerWebConfiguration;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import edu.obya.blueprint.customer.adapter.jpa.CustomerJpaConfiguration;
import edu.obya.blueprint.customer.adapter.rest.CustomerController;
import edu.obya.blueprint.customer.adapter.rest.CustomerWebConfiguration;
import io.cucumber.spring.CucumberContextConfiguration;
import io.zonky.test.db.AutoConfigureEmbeddedDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package edu.obya.blueprint.customer.at.config;

import edu.obya.blueprint.customer.application.CustomerDto;
import edu.obya.blueprint.customer.domain.Customer;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.domain.CustomerState;
import edu.obya.blueprint.customer.web.CustomerSummary;
import edu.obya.blueprint.customer.domain.model.Customer;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import edu.obya.blueprint.customer.domain.model.CustomerState;
import edu.obya.blueprint.customer.adapter.rest.CustomerSummary;
import io.cucumber.java.DataTableType;
import io.cucumber.java.ParameterType;
import org.springframework.http.HttpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import edu.obya.blueprint.customer.application.CustomerDto;
import edu.obya.blueprint.customer.at.TestUser;
import edu.obya.blueprint.customer.at.infra.TestContext;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import io.cucumber.java.en.When;
import lombok.RequiredArgsConstructor;
import lombok.val;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package edu.obya.blueprint.customer.at.steps;

import edu.obya.blueprint.customer.at.infra.TestContext;
import edu.obya.blueprint.customer.domain.Customer;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.domain.CustomerState;
import edu.obya.blueprint.customer.web.CustomerSummary;
import edu.obya.blueprint.customer.domain.model.Customer;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import edu.obya.blueprint.customer.domain.model.CustomerState;
import edu.obya.blueprint.customer.adapter.rest.CustomerSummary;
import io.cucumber.java.en.Then;
import lombok.RequiredArgsConstructor;
import lombok.val;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package edu.obya.blueprint.customer.at.steps;

import edu.obya.blueprint.customer.domain.Customer;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.domain.model.Customer;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import io.cucumber.java.en.Given;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package edu.obya.blueprint.customer;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import org.junit.jupiter.api.Test;

import java.util.stream.Collectors;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;

public class CustomerArchitectureTest {

final String ROOT_PACKAGE = "edu.obya.blueprint";
final String ALL_FEATURE_PACKAGES = ROOT_PACKAGE + "..";
final String CUSTOMER_FEATURE_PACKAGE = ROOT_PACKAGE + ".customer";

/**
* https://www.archunit.org/userguide/html/000_Index.html#_slices
*/
@Test
public void features_are_isolated_from_each_other() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(ALL_FEATURE_PACKAGES);
SlicesRuleDefinition.slices().assignedFrom(new SliceAssignment() {
@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
if (javaClass.getName().contains(".customer.")) {
return SliceIdentifier.of("customer");
}
return SliceIdentifier.ignore();
}
@Override
public String getDescription() {
return "only feature packages";
}
}).should().notDependOnEachOther().check(importedClasses);
}

/**
* https://www.archunit.org/userguide/html/000_Index.html#_onion_architecture
*/
@Test
public void dependencies_are_oriented_to_the_center() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(ALL_FEATURE_PACKAGES);
onionArchitecture()
.domainModels(CUSTOMER_FEATURE_PACKAGE + ".domain.model..")
.domainServices(CUSTOMER_FEATURE_PACKAGE + ".domain.service..")
.applicationServices(
CUSTOMER_FEATURE_PACKAGE + ".application..",
CUSTOMER_FEATURE_PACKAGE + ".config..",
ROOT_PACKAGE + ".config.."
)
.adapter("rest", CUSTOMER_FEATURE_PACKAGE + ".adapter.rest..")
.adapter("jpa", CUSTOMER_FEATURE_PACKAGE + ".adapter.jpa..")
.check(importedClasses);
}

@Test
public void output_ports_are_defined_as_interfaces() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(ALL_FEATURE_PACKAGES);
classes()
.that().haveSimpleNameEndingWith("Repository")
.should().beInterfaces()
.check(importedClasses);
classes()
.that().haveSimpleNameEndingWith("Client")
.should().beInterfaces()
.allowEmptyShould(true)
.check(importedClasses);
}

/**
* https://www.archunit.org/userguide/html/000_Index.html#_composing_class_rules
*/
@Test
public void repository_adapters_are_named_and_located_correctly() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(ALL_FEATURE_PACKAGES);
importedClasses
.stream()
.filter(clazz -> clazz.getName().endsWith("Repository"))
.collect(Collectors.toSet())
.forEach(clazz ->
classes()
.that().implement(clazz.getName())
.should().haveSimpleNameEndingWith("Adapter")
.andShould().resideInAnyPackage(CUSTOMER_FEATURE_PACKAGE + ".adapter.jpa..")
.allowEmptyShould(true)
.check(importedClasses)
);
}

/**
* https://www.archunit.org/userguide/html/000_Index.html#_composing_class_rules
*/
@Test
public void client_adapters_are_named_and_located_correctly() {
JavaClasses importedClasses = new ClassFileImporter().importPackages(ALL_FEATURE_PACKAGES);
importedClasses
.stream()
.filter(clazz -> clazz.getName().endsWith("Client"))
.collect(Collectors.toSet())
.forEach(clazz ->
classes()
.that().implement(clazz.getName())
.should().haveSimpleNameEndingWith("Adapter")
.andShould().resideInAnyPackage(CUSTOMER_FEATURE_PACKAGE + ".adapter.client..")
.allowEmptyShould(true)
.check(importedClasses)
);
}
}
4 changes: 4 additions & 0 deletions src/archTest/resources/archunit_ignore_patterns.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Because Spring context configuration depends on JPA adapter configuration
.*CustomerEndpointIT.*
# Because Spring context configuration depends on JPA adapter configuration
.*CustomerServiceIT.*
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
import au.com.dius.pact.core.model.V4Pact;
import au.com.dius.pact.core.model.annotations.Pact;
import au.com.dius.pact.core.model.annotations.PactDirectory;
import edu.obya.blueprint.customer.domain.CustomerId;
import edu.obya.blueprint.customer.web.CustomerSummary;
import edu.obya.blueprint.customer.domain.model.CustomerId;
import edu.obya.blueprint.customer.adapter.rest.CustomerSummary;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.web.client.RestTemplate;

import static au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody;
import static edu.obya.blueprint.customer.TestCustomer.*;
import static edu.obya.blueprint.customer.adapter.rest.TestCustomerOut.TEST_CUSTOMER_OUT;
import static edu.obya.blueprint.customer.application.TestCustomerIn.TEST_CUSTOMER_IN;
import static edu.obya.blueprint.customer.domain.model.TestCustomer.*;
import static edu.obya.blueprint.customer.cdc.TestUserTokens.TEST_ADMIN_TOKEN;
import static edu.obya.blueprint.customer.cdc.TestUserTokens.TEST_USER_TOKEN;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
Expand All @@ -38,7 +40,7 @@ public V4Pact getCustomer(PactBuilder builder) {
.given("an existing customer")
.uponReceiving("get existing customer interaction")
.method("GET")
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID.getId()))
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID))
.matchHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE, APPLICATION_JSON_VALUE)
.matchHeader(AUTHORIZATION, BASIC_AUTH_REGEX, TEST_USER_TOKEN)
.willRespondWith()
Expand Down Expand Up @@ -81,7 +83,7 @@ public V4Pact replaceCustomer(PactBuilder builder) {
.given("an existing customer")
.uponReceiving("replace existing customer interaction")
.method("PUT")
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID.getId()))
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID))
.matchHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE, APPLICATION_JSON_VALUE)
.matchHeader(AUTHORIZATION, BASIC_AUTH_REGEX, TEST_USER_TOKEN)
.body(newJsonBody(object -> {
Expand All @@ -101,7 +103,7 @@ public V4Pact removeCustomer(PactBuilder builder) {
.given("an existing customer")
.uponReceiving("remove existing customer interaction")
.method("DELETE")
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID.getId()))
.matchPath(URI_WITH_ID_REGEX, String.format("/customers/%s", TEST_CUSTOMER_ID))
.matchHeader(AUTHORIZATION, BASIC_AUTH_REGEX, TEST_ADMIN_TOKEN)
.willRespondWith()
.status(204)
Expand All @@ -118,7 +120,7 @@ void getCustomer_whenExists(MockServer mockServer) {
}

private CustomerSummary fetchCustomer(RestTemplate restTemplate) {
return restTemplate.getForObject("/customers/{id}", CustomerSummary.class, TEST_CUSTOMER_ID.getId());
return restTemplate.getForObject("/customers/{id}", CustomerSummary.class, TEST_CUSTOMER_ID);
}

@Test
Expand All @@ -131,13 +133,13 @@ void addCustomer(MockServer mockServer) {
@Test
@PactTestFor(pactMethod = "replaceCustomer")
void replaceCustomer(MockServer mockServer) {
templateWithAuth(mockServer).put("/customers/{id}", TEST_CUSTOMER_IN, TEST_CUSTOMER_ID.getId());
templateWithAuth(mockServer).put("/customers/{id}", TEST_CUSTOMER_IN, TEST_CUSTOMER_ID);
}

@Test
@PactTestFor(pactMethod = "removeCustomer")
void removeCustomer(MockServer mockServer) {
templateWithAuthElevatedMode(mockServer).delete("/customers/{id}", TEST_CUSTOMER_ID.getId());
templateWithAuthElevatedMode(mockServer).delete("/customers/{id}", TEST_CUSTOMER_ID);
}

private RestTemplate templateWithAuth(MockServer mockServer) {
Expand Down
Loading

0 comments on commit 6d102e5

Please sign in to comment.