From 48ab8183d9d757f5a01ccf8712079b50e672e88d Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 12:57:10 +0100 Subject: [PATCH 1/8] Migrate to Spring Boot - step 1: dependencies --- adapter/pom.xml | 38 ++++++++++++++++++-------------------- bootstrap/pom.xml | 28 +++++++++------------------- pom.xml | 28 +++++++++------------------- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/adapter/pom.xml b/adapter/pom.xml index 189ccd7..c56fdf2 100644 --- a/adapter/pom.xml +++ b/adapter/pom.xml @@ -23,44 +23,42 @@ - io.quarkus - quarkus-arc + org.springframework.boot + spring-boot-starter - io.quarkus - quarkus-hibernate-orm + org.springframework.boot + spring-boot-starter-web - io.quarkus - quarkus-hibernate-orm-panache + org.springframework.boot + spring-boot-starter-data-jpa - io.quarkus - quarkus-jdbc-mysql + com.mysql + mysql-connector-j + runtime - io.quarkus - quarkus-resteasy - - - io.quarkus - quarkus-resteasy-jackson + com.h2database + h2 + runtime - io.quarkus - quarkus-junit5 + org.springframework.boot + spring-boot-starter-test test - io.quarkus - quarkus-junit5-mockito + io.rest-assured + rest-assured test - io.rest-assured - rest-assured + org.testcontainers + mysql test diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml index 1328237..5e3a376 100644 --- a/bootstrap/pom.xml +++ b/bootstrap/pom.xml @@ -34,13 +34,8 @@ - io.quarkus - quarkus-junit5 - test - - - io.quarkus - quarkus-junit5-mockito + org.springframework.boot + spring-boot-starter-test test @@ -53,6 +48,11 @@ rest-assured test + + org.testcontainers + mysql + test + @@ -68,18 +68,8 @@ - io.quarkus.platform - quarkus-maven-plugin - true - - - - build - generate-code - generate-code-tests - - - + org.springframework.boot + spring-boot-maven-plugin diff --git a/pom.xml b/pom.xml index 57c951a..a50cc6d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,6 +4,12 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + eu.happycoders.shop parent 1.0-SNAPSHOT @@ -22,7 +28,6 @@ 20 UTF-8 - 3.4.3 6.55.0 @@ -50,34 +55,25 @@ org.junit.jupiter junit-jupiter - + test org.assertj assertj-core - 3.24.2 + test org.mockito mockito-core - + test - - - io.quarkus.platform - quarkus-bom - ${quarkus.platform.version} - pom - import - - com.tngtech.archunit archunit-junit5 @@ -88,12 +84,6 @@ - - io.quarkus.platform - quarkus-maven-plugin - ${quarkus.platform.version} - - From 1378747afa1a8d3f30a1f2495dbfd9554f24110d Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 13:39:52 +0100 Subject: [PATCH 2/8] Migrate to Spring Boot - step 2: REST adapters --- .../in/rest/cart/AddToCartController.java | 31 ++++++++-------- .../in/rest/cart/EmptyCartController.java | 20 +++++------ .../in/rest/cart/GetCartController.java | 18 +++++----- .../in/rest/common/ClientErrorException.java | 24 +++++++++++++ .../in/rest/common/ClientErrorHandler.java | 19 ++++++++++ .../in/rest/common/ControllerCommons.java | 12 +++---- .../in/rest/common/CustomerIdParser.java | 8 ++--- .../in/rest/common/ProductIdParser.java | 10 +++--- .../rest/product/FindProductsController.java | 24 ++++++------- .../shop/adapter/in/rest/HttpTestCommons.java | 9 +++-- .../rest/cart/CartsControllerAssertions.java | 4 +-- .../in/rest/cart/CartsControllerTest.java | 35 ++++++++++++------- .../product/ProductsControllerAssertions.java | 6 ++-- .../rest/product/ProductsControllerTest.java | 22 +++++++----- 14 files changed, 148 insertions(+), 94 deletions(-) create mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java create mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java index 27bbc27..68bb441 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/AddToCartController.java @@ -10,21 +10,20 @@ import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException; import eu.happycoders.shop.model.customer.CustomerId; import eu.happycoders.shop.model.product.ProductId; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * REST controller for all shopping cart use cases. * * @author Sven Woltmann */ -@Path("/carts") -@Produces(MediaType.APPLICATION_JSON) +@RestController +@RequestMapping("/carts") public class AddToCartController { private final AddToCartUseCase addToCartUseCase; @@ -33,12 +32,11 @@ public AddToCartController(AddToCartUseCase addToCartUseCase) { this.addToCartUseCase = addToCartUseCase; } - @POST - @Path("/{customerId}/line-items") + @PostMapping("/{customerId}/line-items") public CartWebModel addLineItem( - @PathParam("customerId") String customerIdString, - @QueryParam("productId") String productIdString, - @QueryParam("quantity") int quantity) { + @PathVariable("customerId") String customerIdString, + @RequestParam("productId") String productIdString, + @RequestParam("quantity") int quantity) { CustomerId customerId = parseCustomerId(customerIdString); ProductId productId = parseProductId(productIdString); @@ -46,11 +44,10 @@ public CartWebModel addLineItem( Cart cart = addToCartUseCase.addToCart(customerId, productId, quantity); return CartWebModel.fromDomainModel(cart); } catch (ProductNotFoundException e) { - throw clientErrorException( - Response.Status.BAD_REQUEST, "The requested product does not exist"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "The requested product does not exist"); } catch (NotEnoughItemsInStockException e) { throw clientErrorException( - Response.Status.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock())); + HttpStatus.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock())); } } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java index 16e089b..be98b94 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/EmptyCartController.java @@ -4,19 +4,19 @@ import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase; import eu.happycoders.shop.model.customer.CustomerId; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * REST controller for all shopping cart use cases. * * @author Sven Woltmann */ -@Path("/carts") -@Produces(MediaType.APPLICATION_JSON) +@RestController +@RequestMapping("/carts") public class EmptyCartController { private final EmptyCartUseCase emptyCartUseCase; @@ -25,10 +25,10 @@ public EmptyCartController(EmptyCartUseCase emptyCartUseCase) { this.emptyCartUseCase = emptyCartUseCase; } - @DELETE - @Path("/{customerId}") - public void deleteCart(@PathParam("customerId") String customerIdString) { + @DeleteMapping("/{customerId}") + public ResponseEntity deleteCart(@PathVariable("customerId") String customerIdString) { CustomerId customerId = parseCustomerId(customerIdString); emptyCartUseCase.emptyCart(customerId); + return ResponseEntity.noContent().build(); } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java index 515cd6f..da79381 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/cart/GetCartController.java @@ -5,19 +5,18 @@ import eu.happycoders.shop.application.port.in.cart.GetCartUseCase; import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.customer.CustomerId; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** * REST controller for all shopping cart use cases. * * @author Sven Woltmann */ -@Path("/carts") -@Produces(MediaType.APPLICATION_JSON) +@RestController +@RequestMapping("/carts") public class GetCartController { private final GetCartUseCase getCartUseCase; @@ -26,9 +25,8 @@ public GetCartController(GetCartUseCase getCartUseCase) { this.getCartUseCase = getCartUseCase; } - @GET - @Path("/{customerId}") - public CartWebModel getCart(@PathParam("customerId") String customerIdString) { + @GetMapping("/{customerId}") + public CartWebModel getCart(@PathVariable("customerId") String customerIdString) { CustomerId customerId = parseCustomerId(customerIdString); Cart cart = getCartUseCase.getCart(customerId); return CartWebModel.fromDomainModel(cart); diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java new file mode 100644 index 0000000..fae718b --- /dev/null +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorException.java @@ -0,0 +1,24 @@ +package eu.happycoders.shop.adapter.in.rest.common; + +import lombok.Getter; +import org.springframework.http.ResponseEntity; + +/** + * An exception to be thrown in case of a client error (e.g., invalid input). + * + * @author Sven Woltmann + */ +public class ClientErrorException extends RuntimeException { + + @Getter private final ResponseEntity response; + + public ClientErrorException(ResponseEntity response) { + super(getMessage(response)); + this.response = response; + } + + private static String getMessage(ResponseEntity response) { + ErrorEntity body = response.getBody(); + return body != null ? body.errorMessage() : null; + } +} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java new file mode 100644 index 0000000..06e6f03 --- /dev/null +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ClientErrorHandler.java @@ -0,0 +1,19 @@ +package eu.happycoders.shop.adapter.in.rest.common; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * Handles {@link ClientErrorException} by returning a JSON body containing the error details. + * + * @author Sven Woltmann + */ +@RestControllerAdvice +public class ClientErrorHandler { + + @ExceptionHandler(ClientErrorException.class) + public ResponseEntity handleProductNotFoundException(ClientErrorException ex) { + return ex.getResponse(); + } +} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java index bac53c4..ea6d133 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ControllerCommons.java @@ -1,7 +1,7 @@ package eu.happycoders.shop.adapter.in.rest.common; -import jakarta.ws.rs.ClientErrorException; -import jakarta.ws.rs.core.Response; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; /** * Common functionality for all REST controllers. @@ -12,12 +12,12 @@ public final class ControllerCommons { private ControllerCommons() {} - public static ClientErrorException clientErrorException(Response.Status status, String message) { + public static ClientErrorException clientErrorException(HttpStatus status, String message) { return new ClientErrorException(errorResponse(status, message)); } - public static Response errorResponse(Response.Status status, String message) { - ErrorEntity errorEntity = new ErrorEntity(status.getStatusCode(), message); - return Response.status(status).entity(errorEntity).build(); + public static ResponseEntity errorResponse(HttpStatus status, String message) { + ErrorEntity errorEntity = new ErrorEntity(status.value(), message); + return ResponseEntity.status(status.value()).body(errorEntity); } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java index 5ba9ba6..edc8483 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/CustomerIdParser.java @@ -3,11 +3,11 @@ import static eu.happycoders.shop.adapter.in.rest.common.ControllerCommons.clientErrorException; import eu.happycoders.shop.model.customer.CustomerId; -import jakarta.ws.rs.core.Response; +import org.springframework.http.HttpStatus; /** - * A parser for customer IDs, throwing a {@link jakarta.ws.rs.ClientErrorException} for invalid - * customer IDs. + * A parser for customer IDs, throwing a {@link + * org.springframework.web.client.HttpClientErrorException} for invalid customer IDs. * * @author Sven Woltmann */ @@ -19,7 +19,7 @@ public static CustomerId parseCustomerId(String string) { try { return new CustomerId(Integer.parseInt(string)); } catch (IllegalArgumentException e) { - throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'customerId'"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'customerId'"); } } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java index 5aabeae..cd3110a 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/common/ProductIdParser.java @@ -3,11 +3,11 @@ import static eu.happycoders.shop.adapter.in.rest.common.ControllerCommons.clientErrorException; import eu.happycoders.shop.model.product.ProductId; -import jakarta.ws.rs.core.Response; +import org.springframework.http.HttpStatus; /** - * A parser for product IDs, throwing a {@link jakarta.ws.rs.ClientErrorException} for invalid - * product IDs. + * A parser for product IDs, throwing a {@link + * org.springframework.web.client.HttpClientErrorException} for invalid product IDs. * * @author Sven Woltmann */ @@ -17,13 +17,13 @@ private ProductIdParser() {} public static ProductId parseProductId(String string) { if (string == null) { - throw clientErrorException(Response.Status.BAD_REQUEST, "Missing 'productId'"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "Missing 'productId'"); } try { return new ProductId(string); } catch (IllegalArgumentException e) { - throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'productId'"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'productId'"); } } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java index 2f4f11f..1a6466b 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/in/rest/product/FindProductsController.java @@ -4,21 +4,20 @@ import eu.happycoders.shop.application.port.in.product.FindProductsUseCase; import eu.happycoders.shop.model.product.Product; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * REST controller for all product use cases. * * @author Sven Woltmann */ -@Path("/products") -@Produces(MediaType.APPLICATION_JSON) +@RestController +@RequestMapping("/products") public class FindProductsController { private final FindProductsUseCase findProductsUseCase; @@ -27,10 +26,11 @@ public FindProductsController(FindProductsUseCase findProductsUseCase) { this.findProductsUseCase = findProductsUseCase; } - @GET - public List findProducts(@QueryParam("query") String query) { + @GetMapping + public List findProducts( + @RequestParam(value = "query", required = false) String query) { if (query == null) { - throw clientErrorException(Response.Status.BAD_REQUEST, "Missing 'query'"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "Missing 'query'"); } List products; @@ -38,7 +38,7 @@ public List findProducts(@QueryParam("query") String quer try { products = findProductsUseCase.findByNameOrDescription(query); } catch (IllegalArgumentException e) { - throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'query'"); + throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'query'"); } return products.stream().map(ProductInListWebModel::fromDomainModel).toList(); diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java index 12a181d..2662ed4 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java @@ -4,20 +4,19 @@ import io.restassured.path.json.JsonPath; import io.restassured.response.Response; +import org.springframework.http.HttpStatus; public final class HttpTestCommons { private HttpTestCommons() {} public static void assertThatResponseIsError( - Response response, - jakarta.ws.rs.core.Response.Status expectedStatus, - String expectedErrorMessage) { - assertThat(response.getStatusCode()).isEqualTo(expectedStatus.getStatusCode()); + Response response, HttpStatus expectedStatus, String expectedErrorMessage) { + assertThat(response.getStatusCode()).isEqualTo(expectedStatus.value()); JsonPath json = response.jsonPath(); - assertThat(json.getInt("httpStatus")).isEqualTo(expectedStatus.getStatusCode()); + assertThat(json.getInt("httpStatus")).isEqualTo(expectedStatus.value()); assertThat(json.getString("errorMessage")).isEqualTo(expectedErrorMessage); } } diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java index 65c62b7..db9d17e 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerAssertions.java @@ -1,7 +1,7 @@ package eu.happycoders.shop.adapter.in.rest.cart; -import static jakarta.ws.rs.core.Response.Status.OK; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.OK; import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.cart.CartLineItem; @@ -13,7 +13,7 @@ public final class CartsControllerAssertions { private CartsControllerAssertions() {} public static void assertThatResponseIsCart(Response response, Cart cart) { - assertThat(response.statusCode()).isEqualTo(OK.getStatusCode()); + assertThat(response.statusCode()).isEqualTo(OK.value()); JsonPath json = response.jsonPath(); diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java index 2e2b48a..cd88f81 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java @@ -5,10 +5,10 @@ import static eu.happycoders.shop.model.money.TestMoneyFactory.euros; import static eu.happycoders.shop.model.product.TestProductFactory.createTestProduct; import static io.restassured.RestAssured.given; -import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; -import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NO_CONTENT; import eu.happycoders.shop.application.port.in.cart.AddToCartUseCase; import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase; @@ -19,27 +19,33 @@ import eu.happycoders.shop.model.customer.CustomerId; import eu.happycoders.shop.model.product.Product; import eu.happycoders.shop.model.product.ProductId; -import io.quarkus.test.InjectMock; -import io.quarkus.test.junit.QuarkusTest; import io.restassured.response.Response; import org.junit.jupiter.api.Test; - -@QuarkusTest +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class CartsControllerTest { private static final CustomerId TEST_CUSTOMER_ID = new CustomerId(61157); private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99)); private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99)); - @InjectMock AddToCartUseCase addToCartUseCase; - @InjectMock GetCartUseCase getCartUseCase; - @InjectMock EmptyCartUseCase emptyCartUseCase; + @LocalServerPort private Integer port; + + @MockBean AddToCartUseCase addToCartUseCase; + @MockBean GetCartUseCase getCartUseCase; + @MockBean EmptyCartUseCase emptyCartUseCase; @Test void givenASyntacticallyInvalidCustomerId_getCart_returnsAnError() { String customerId = "foo"; - Response response = given().get("/carts/" + customerId).then().extract().response(); + Response response = given().port(port).get("/carts/" + customerId).then().extract().response(); assertThatResponseIsError(response, BAD_REQUEST, "Invalid 'customerId'"); } @@ -55,7 +61,8 @@ void givenAValidCustomerIdAndACart_getCart_requestsCartFromUseCaseAndReturnsIt() when(getCartUseCase.getCart(customerId)).thenReturn(cart); - Response response = given().get("/carts/" + customerId.value()).then().extract().response(); + Response response = + given().port(port).get("/carts/" + customerId.value()).then().extract().response(); assertThatResponseIsCart(response, cart); } @@ -74,6 +81,7 @@ void givenSomeTestData_addLineItem_invokesAddToCartUseCaseAndReturnsUpdatedCart( Response response = given() + .port(port) .queryParam("productId", productId.value()) .queryParam("quantity", quantity) .post("/carts/" + customerId.value() + "/line-items") @@ -92,6 +100,7 @@ void givenAnInvalidProductId_addLineItem_returnsAnError() { Response response = given() + .port(port) .queryParam("productId", productId) .queryParam("quantity", quantity) .post("/carts/" + customerId.value() + "/line-items") @@ -114,6 +123,7 @@ void givenProductNotFound_addLineItem_returnsAnError() Response response = given() + .port(port) .queryParam("productId", productId.value()) .queryParam("quantity", quantity) .post("/carts/" + customerId.value() + "/line-items") @@ -136,6 +146,7 @@ void givenNotEnoughItemsInStock_addLineItem_returnsAnError() Response response = given() + .port(port) .queryParam("productId", productId.value()) .queryParam("quantity", quantity) .post("/carts/" + customerId.value() + "/line-items") @@ -150,7 +161,7 @@ void givenNotEnoughItemsInStock_addLineItem_returnsAnError() void givenACustomerId_deleteCart_invokesDeleteCartUseCaseAndReturnsUpdatedCart() { CustomerId customerId = TEST_CUSTOMER_ID; - given().delete("/carts/" + customerId.value()).then().statusCode(NO_CONTENT.getStatusCode()); + given().port(port).delete("/carts/" + customerId.value()).then().statusCode(NO_CONTENT.value()); verify(emptyCartUseCase).emptyCart(customerId); } diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java index e335f1d..6e89d64 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerAssertions.java @@ -1,7 +1,7 @@ package eu.happycoders.shop.adapter.in.rest.product; -import static jakarta.ws.rs.core.Response.Status.OK; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpStatus.OK; import eu.happycoders.shop.model.product.Product; import io.restassured.path.json.JsonPath; @@ -13,7 +13,7 @@ public final class ProductsControllerAssertions { private ProductsControllerAssertions() {} public static void assertThatResponseIsProduct(Response response, Product product) { - assertThat(response.statusCode()).isEqualTo(OK.getStatusCode()); + assertThat(response.statusCode()).isEqualTo(OK.value()); JsonPath json = response.jsonPath(); @@ -21,7 +21,7 @@ public static void assertThatResponseIsProduct(Response response, Product produc } public static void assertThatResponseIsProductList(Response response, List products) { - assertThat(response.statusCode()).isEqualTo(OK.getStatusCode()); + assertThat(response.statusCode()).isEqualTo(OK.value()); JsonPath json = response.jsonPath(); diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java index fad9dab..a0cd187 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/product/ProductsControllerTest.java @@ -5,24 +5,30 @@ import static eu.happycoders.shop.model.money.TestMoneyFactory.euros; import static eu.happycoders.shop.model.product.TestProductFactory.createTestProduct; import static io.restassured.RestAssured.given; -import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import eu.happycoders.shop.application.port.in.product.FindProductsUseCase; import eu.happycoders.shop.model.product.Product; -import io.quarkus.test.InjectMock; -import io.quarkus.test.junit.QuarkusTest; import io.restassured.response.Response; import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ProductsControllerTest { private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99)); private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99)); - @InjectMock FindProductsUseCase findProductsUseCase; + @LocalServerPort private Integer port; + + @MockBean FindProductsUseCase findProductsUseCase; @Test void givenAQueryAndAListOfProducts_findProducts_requestsProductsViaQueryAndReturnsThem() { @@ -32,14 +38,14 @@ void givenAQueryAndAListOfProducts_findProducts_requestsProductsViaQueryAndRetur when(findProductsUseCase.findByNameOrDescription(query)).thenReturn(productList); Response response = - given().queryParam("query", query).get("/products").then().extract().response(); + given().port(port).queryParam("query", query).get("/products").then().extract().response(); assertThatResponseIsProductList(response, productList); } @Test void givenANullQuery_findProducts_returnsError() { - Response response = given().get("/products").then().extract().response(); + Response response = given().port(port).get("/products").then().extract().response(); assertThatResponseIsError(response, BAD_REQUEST, "Missing 'query'"); } @@ -51,7 +57,7 @@ void givenATooShortQuery_findProducts_returnsError() { .thenThrow(IllegalArgumentException.class); Response response = - given().queryParam("query", query).get("/products").then().extract().response(); + given().port(port).queryParam("query", query).get("/products").then().extract().response(); assertThatResponseIsError(response, BAD_REQUEST, "Invalid 'query'"); } From 1ee433f28074752b03b600a2fe22555cf703c99f Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 13:44:45 +0100 Subject: [PATCH 3/8] Migrate to Spring Boot - step 3: persistence adapters --- .../inmemory/InMemoryCartRepository.java | 8 +++---- .../inmemory/InMemoryProductRepository.java | 8 +++---- .../out/persistence/jpa/CartMapper.java | 9 ++----- .../jpa/JpaCartPanacheRepository.java | 12 ---------- .../persistence/jpa/JpaCartRepository.java | 22 ++++++++--------- .../jpa/JpaCartSpringDataRepository.java | 12 ++++++++++ .../jpa/JpaProductPanacheRepository.java | 13 ---------- .../persistence/jpa/JpaProductRepository.java | 24 +++++++++---------- .../jpa/JpaProductSpringDataRepository.java | 18 ++++++++++++++ .../out/persistence/jpa/ProductMapper.java | 5 ---- .../shop/adapter/TestProfileWithMySQL.java | 12 ---------- .../AbstractCartRepositoryTest.java | 11 +++------ .../AbstractProductRepositoryTest.java | 13 ++-------- .../inmemory/InMemoryCartRepositoryTest.java | 6 +++-- .../InMemoryProductRepositoryTest.java | 6 +++-- .../jpa/JpaCartRepositoryTest.java | 9 ++++--- .../jpa/JpaProductRepositoryTest.java | 9 ++++--- .../application-test-with-mysql.properties | 5 ++++ .../resources/application-test.properties | 1 + 19 files changed, 89 insertions(+), 114 deletions(-) delete mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java create mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java delete mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java create mode 100644 adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java delete mode 100644 adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java create mode 100644 adapter/src/test/resources/application-test-with-mysql.properties create mode 100644 adapter/src/test/resources/application-test.properties diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java index 0cb5baa..db5a386 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java @@ -3,19 +3,19 @@ import eu.happycoders.shop.application.port.out.persistence.CartRepository; import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.customer.CustomerId; -import io.quarkus.arc.lookup.LookupIfProperty; -import jakarta.enterprise.context.ApplicationScoped; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; /** * Persistence adapter: Stores carts in memory. * * @author Sven Woltmann */ -@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true) -@ApplicationScoped +@ConditionalOnProperty(name = "persistence", havingValue = "inmemory", matchIfMissing = true) +@Repository public class InMemoryCartRepository implements CartRepository { private final Map carts = new ConcurrentHashMap<>(); diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java index 3179707..8691411 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java @@ -4,21 +4,21 @@ import eu.happycoders.shop.application.port.out.persistence.ProductRepository; import eu.happycoders.shop.model.product.Product; import eu.happycoders.shop.model.product.ProductId; -import io.quarkus.arc.lookup.LookupIfProperty; -import jakarta.enterprise.context.ApplicationScoped; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; /** * Persistence adapter: Stores products in memory. * * @author Sven Woltmann */ -@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true) -@ApplicationScoped +@ConditionalOnProperty(name = "persistence", havingValue = "inmemory", matchIfMissing = true) +@Repository public class InMemoryProductRepository implements ProductRepository { private final Map products = new ConcurrentHashMap<>(); diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java index 91315dd..6250edb 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/CartMapper.java @@ -3,7 +3,6 @@ import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.cart.CartLineItem; import eu.happycoders.shop.model.customer.CustomerId; -import java.util.Optional; /** * Maps model carts and line items to JPA carts and line items - and vice versa. @@ -36,11 +35,7 @@ static CartLineItemJpaEntity toJpaEntity(CartJpaEntity cartJpaEntity, CartLineIt return entity; } - static Optional toModelEntityOptional(CartJpaEntity cartJpaEntity) { - if (cartJpaEntity == null) { - return Optional.empty(); - } - + static Cart toModelEntity(CartJpaEntity cartJpaEntity) { CustomerId customerId = new CustomerId(cartJpaEntity.getCustomerId()); Cart cart = new Cart(customerId); @@ -50,6 +45,6 @@ static Optional toModelEntityOptional(CartJpaEntity cartJpaEntity) { lineItemJpaEntity.getQuantity()); } - return Optional.of(cart); + return cart; } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java deleted file mode 100644 index f565e3c..0000000 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.happycoders.shop.adapter.out.persistence.jpa; - -import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Panache repository for {@link CartJpaEntity}. - * - * @author Sven Woltmann - */ -@ApplicationScoped -public class JpaCartPanacheRepository implements PanacheRepositoryBase {} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java index c5192e3..6c93b09 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java @@ -3,42 +3,42 @@ import eu.happycoders.shop.application.port.out.persistence.CartRepository; import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.customer.CustomerId; -import io.quarkus.arc.lookup.LookupIfProperty; -import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; import java.util.Optional; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; /** * Persistence adapter: Stores carts via JPA in a database. * * @author Sven Woltmann */ -@LookupIfProperty(name = "persistence", stringValue = "mysql") -@ApplicationScoped +@ConditionalOnProperty(name = "persistence", havingValue = "mysql") +@Repository public class JpaCartRepository implements CartRepository { - private final JpaCartPanacheRepository panacheRepository; + private final JpaCartSpringDataRepository springDataRepository; - public JpaCartRepository(JpaCartPanacheRepository panacheRepository) { - this.panacheRepository = panacheRepository; + public JpaCartRepository(JpaCartSpringDataRepository springDataRepository) { + this.springDataRepository = springDataRepository; } @Override @Transactional public void save(Cart cart) { - panacheRepository.getEntityManager().merge(CartMapper.toJpaEntity(cart)); + springDataRepository.save(CartMapper.toJpaEntity(cart)); } @Override @Transactional public Optional findByCustomerId(CustomerId customerId) { - CartJpaEntity cartJpaEntity = panacheRepository.findById(customerId.value()); - return CartMapper.toModelEntityOptional(cartJpaEntity); + Optional cartJpaEntity = springDataRepository.findById(customerId.value()); + return cartJpaEntity.map(CartMapper::toModelEntity); } @Override @Transactional public void deleteByCustomerId(CustomerId customerId) { - panacheRepository.deleteById(customerId.value()); + springDataRepository.deleteById(customerId.value()); } } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java new file mode 100644 index 0000000..78f1456 --- /dev/null +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartSpringDataRepository.java @@ -0,0 +1,12 @@ +package eu.happycoders.shop.adapter.out.persistence.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Spring Data repository for {@link CartJpaEntity}. + * + * @author Sven Woltmann + */ +@Repository +public interface JpaCartSpringDataRepository extends JpaRepository {} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java deleted file mode 100644 index 96d855c..0000000 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.happycoders.shop.adapter.out.persistence.jpa; - -import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Panache repository for {@link ProductJpaEntity}. - * - * @author Sven Woltmann - */ -@ApplicationScoped -public class JpaProductPanacheRepository - implements PanacheRepositoryBase {} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java index 2d7eb0a..a385645 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java @@ -4,26 +4,26 @@ import eu.happycoders.shop.application.port.out.persistence.ProductRepository; import eu.happycoders.shop.model.product.Product; import eu.happycoders.shop.model.product.ProductId; -import io.quarkus.arc.lookup.LookupIfProperty; import jakarta.annotation.PostConstruct; -import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Repository; /** * Persistence adapter: Stores products via JPA in a database. * * @author Sven Woltmann */ -@LookupIfProperty(name = "persistence", stringValue = "mysql") -@ApplicationScoped +@ConditionalOnProperty(name = "persistence", havingValue = "mysql") +@Repository public class JpaProductRepository implements ProductRepository { - private final JpaProductPanacheRepository panacheRepository; + private final JpaProductSpringDataRepository springDataRepository; - public JpaProductRepository(JpaProductPanacheRepository panacheRepository) { - this.panacheRepository = panacheRepository; + public JpaProductRepository(JpaProductSpringDataRepository springDataRepository) { + this.springDataRepository = springDataRepository; } @PostConstruct @@ -34,23 +34,21 @@ void createDemoProducts() { @Override @Transactional public void save(Product product) { - panacheRepository.getEntityManager().merge(ProductMapper.toJpaEntity(product)); + springDataRepository.save(ProductMapper.toJpaEntity(product)); } @Override @Transactional public Optional findById(ProductId productId) { - ProductJpaEntity jpaEntity = panacheRepository.findById(productId.value()); - return ProductMapper.toModelEntityOptional(jpaEntity); + Optional jpaEntity = springDataRepository.findById(productId.value()); + return jpaEntity.map(ProductMapper::toModelEntity); } @Override @Transactional public List findByNameOrDescription(String queryString) { List entities = - panacheRepository - .find("name like ?1 or description like ?1", "%" + queryString + "%") - .list(); + springDataRepository.findByNameOrDescriptionLike("%" + queryString + "%"); return ProductMapper.toModelEntities(entities); } diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java new file mode 100644 index 0000000..3dc3649 --- /dev/null +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductSpringDataRepository.java @@ -0,0 +1,18 @@ +package eu.happycoders.shop.adapter.out.persistence.jpa; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +/** + * Spring Data repository for {@link ProductJpaEntity}. + * + * @author Sven Woltmann + */ +@Repository +public interface JpaProductSpringDataRepository extends JpaRepository { + + @Query("SELECT p FROM ProductJpaEntity p WHERE p.name like ?1 or p.description like ?1") + List findByNameOrDescriptionLike(String pattern); +} diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java index 50fb7be..35d2e92 100644 --- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java +++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/ProductMapper.java @@ -5,7 +5,6 @@ import eu.happycoders.shop.model.product.ProductId; import java.util.Currency; import java.util.List; -import java.util.Optional; /** * Maps a model product to a JPA product and vice versa. @@ -29,10 +28,6 @@ static ProductJpaEntity toJpaEntity(Product product) { return jpaEntity; } - static Optional toModelEntityOptional(ProductJpaEntity jpaEntity) { - return Optional.ofNullable(jpaEntity).map(ProductMapper::toModelEntity); - } - static Product toModelEntity(ProductJpaEntity jpaEntity) { return new Product( new ProductId(jpaEntity.getId()), diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java b/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java deleted file mode 100644 index c097e95..0000000 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java +++ /dev/null @@ -1,12 +0,0 @@ -package eu.happycoders.shop.adapter; - -import io.quarkus.test.junit.QuarkusTestProfile; -import java.util.Map; - -public class TestProfileWithMySQL implements QuarkusTestProfile { - - @Override - public Map getConfigOverrides() { - return Map.of("persistence", "mysql"); - } -} diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java index 7a8a0d8..e75d4ef 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractCartRepositoryTest.java @@ -11,12 +11,11 @@ import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException; import eu.happycoders.shop.model.customer.CustomerId; import eu.happycoders.shop.model.product.Product; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; public abstract class AbstractCartRepositoryTest { @@ -25,20 +24,16 @@ public abstract class AbstractCartRepositoryTest { private static final AtomicInteger CUSTOMER_ID_SEQUENCE_GENERATOR = new AtomicInteger(); - @Inject Instance cartRepositoryInstance; + @Autowired CartRepository cartRepository; - @Inject Instance productRepositoryInstance; - - private CartRepository cartRepository; + @Autowired ProductRepository productRepository; @BeforeEach void initRepositories() { - cartRepository = cartRepositoryInstance.get(); persistTestProducts(); } private void persistTestProducts() { - ProductRepository productRepository = productRepositoryInstance.get(); productRepository.save(TEST_PRODUCT_1); productRepository.save(TEST_PRODUCT_2); } diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java index c8c7c39..ef6e5c7 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/AbstractProductRepositoryTest.java @@ -5,23 +5,14 @@ import eu.happycoders.shop.application.port.out.persistence.ProductRepository; import eu.happycoders.shop.model.product.Product; import eu.happycoders.shop.model.product.ProductId; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; public abstract class AbstractProductRepositoryTest { - @Inject Instance productRepositoryInstance; - - private ProductRepository productRepository; - - @BeforeEach - void initRepository() { - productRepository = productRepositoryInstance.get(); - } + @Autowired ProductRepository productRepository; @Test void givenTestProductsAndATestProductId_findById_returnsATestProduct() { diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java index 7df6d04..5dfd4de 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepositoryTest.java @@ -1,7 +1,9 @@ package eu.happycoders.shop.adapter.out.persistence.inmemory; import eu.happycoders.shop.adapter.out.persistence.AbstractCartRepositoryTest; -import io.quarkus.test.junit.QuarkusTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest +@SpringBootTest +@ActiveProfiles("test") class InMemoryCartRepositoryTest extends AbstractCartRepositoryTest {} diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java index 6d42eb8..c99e569 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepositoryTest.java @@ -1,7 +1,9 @@ package eu.happycoders.shop.adapter.out.persistence.inmemory; import eu.happycoders.shop.adapter.out.persistence.AbstractProductRepositoryTest; -import io.quarkus.test.junit.QuarkusTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest +@SpringBootTest +@ActiveProfiles("test") class InMemoryProductRepositoryTest extends AbstractProductRepositoryTest {} diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java index 4b68780..93f29e0 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepositoryTest.java @@ -1,10 +1,9 @@ package eu.happycoders.shop.adapter.out.persistence.jpa; -import eu.happycoders.shop.adapter.TestProfileWithMySQL; import eu.happycoders.shop.adapter.out.persistence.AbstractCartRepositoryTest; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest -@TestProfile(TestProfileWithMySQL.class) +@SpringBootTest +@ActiveProfiles("test-with-mysql") class JpaCartRepositoryTest extends AbstractCartRepositoryTest {} diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java index bd20969..ed633e8 100644 --- a/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java +++ b/adapter/src/test/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepositoryTest.java @@ -1,10 +1,9 @@ package eu.happycoders.shop.adapter.out.persistence.jpa; -import eu.happycoders.shop.adapter.TestProfileWithMySQL; import eu.happycoders.shop.adapter.out.persistence.AbstractProductRepositoryTest; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest -@TestProfile(TestProfileWithMySQL.class) +@SpringBootTest +@ActiveProfiles("test-with-mysql") class JpaProductRepositoryTest extends AbstractProductRepositoryTest {} diff --git a/adapter/src/test/resources/application-test-with-mysql.properties b/adapter/src/test/resources/application-test-with-mysql.properties new file mode 100644 index 0000000..cdb8696 --- /dev/null +++ b/adapter/src/test/resources/application-test-with-mysql.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:tc:mysql:8.0:///shop + +spring.jpa.hibernate.ddl-auto=update + +persistence=mysql diff --git a/adapter/src/test/resources/application-test.properties b/adapter/src/test/resources/application-test.properties new file mode 100644 index 0000000..71c3bde --- /dev/null +++ b/adapter/src/test/resources/application-test.properties @@ -0,0 +1 @@ +spring.datasource.url=jdbc:h2:mem:testdb From 8f86ab11e8a682d85b58b6c8d23d4e7b73a3ee2c Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 13:45:07 +0100 Subject: [PATCH 4/8] Migrate to Spring Boot - step 4: bean configuration --- ...kusAppConfig.java => SpringAppConfig.java} | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) rename adapter/src/main/java/eu/happycoders/shop/{QuarkusAppConfig.java => SpringAppConfig.java} (56%) diff --git a/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java b/adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java similarity index 56% rename from adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java rename to adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java index 9eacf0f..017563e 100644 --- a/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java +++ b/adapter/src/main/java/eu/happycoders/shop/SpringAppConfig.java @@ -10,38 +10,40 @@ import eu.happycoders.shop.application.service.cart.EmptyCartService; import eu.happycoders.shop.application.service.cart.GetCartService; import eu.happycoders.shop.application.service.product.FindProductsService; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; -class QuarkusAppConfig { +/** + * Spring application configuration, making Spring beans from services defined in application + * module. + * + * @author Sven Woltmann + */ +@SpringBootApplication +public class SpringAppConfig { - @Inject Instance cartRepository; + @Autowired CartRepository cartRepository; - @Inject Instance productRepository; + @Autowired ProductRepository productRepository; - @Produces - @ApplicationScoped + @Bean GetCartUseCase getCartUseCase() { - return new GetCartService(cartRepository.get()); + return new GetCartService(cartRepository); } - @Produces - @ApplicationScoped + @Bean EmptyCartUseCase emptyCartUseCase() { - return new EmptyCartService(cartRepository.get()); + return new EmptyCartService(cartRepository); } - @Produces - @ApplicationScoped + @Bean FindProductsUseCase findProductsUseCase() { - return new FindProductsService(productRepository.get()); + return new FindProductsService(productRepository); } - @Produces - @ApplicationScoped + @Bean AddToCartUseCase addToCartUseCase() { - return new AddToCartService(cartRepository.get(), productRepository.get()); + return new AddToCartService(cartRepository, productRepository); } } From 97b1dfdf3af50301f02664b011f302beab839a75 Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 13:45:27 +0100 Subject: [PATCH 5/8] Migrate to Spring Boot - step 5: launcher and end-to-end tests --- .../happycoders/shop/bootstrap/Launcher.java | 16 +++++++++++++ .../resources/application-mysql.properties | 7 ++++++ .../src/main/resources/application.properties | 13 ++--------- .../shop/bootstrap/e2e/CartTest.java | 23 +++++++++++-------- .../shop/bootstrap/e2e/FindProductsTest.java | 15 +++++++----- 5 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java create mode 100644 bootstrap/src/main/resources/application-mysql.properties diff --git a/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java b/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java new file mode 100644 index 0000000..35932f7 --- /dev/null +++ b/bootstrap/src/main/java/eu/happycoders/shop/bootstrap/Launcher.java @@ -0,0 +1,16 @@ +package eu.happycoders.shop.bootstrap; + +import eu.happycoders.shop.SpringAppConfig; +import org.springframework.boot.SpringApplication; + +/** + * Launcher for the application: starts the Spring application. + * + * @author Sven Woltmann + */ +public class Launcher { + + public static void main(String[] args) { + SpringApplication.run(SpringAppConfig.class, args); + } +} diff --git a/bootstrap/src/main/resources/application-mysql.properties b/bootstrap/src/main/resources/application-mysql.properties new file mode 100644 index 0000000..da26d08 --- /dev/null +++ b/bootstrap/src/main/resources/application-mysql.properties @@ -0,0 +1,7 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/shop +spring.datasource.username=root +spring.datasource.password=test + +spring.jpa.hibernate.ddl-auto=update + +persistence=mysql diff --git a/bootstrap/src/main/resources/application.properties b/bootstrap/src/main/resources/application.properties index bdb9f17..88c8262 100644 --- a/bootstrap/src/main/resources/application.properties +++ b/bootstrap/src/main/resources/application.properties @@ -1,11 +1,2 @@ -# Without this line, Quarkus would abort with the following error message: -# "Model classes are defined for the default persistence unit but configured datasource not found: -# the default EntityManagerFactory will not be created." -%prod.quarkus.datasource.jdbc.url=dummy -%prod.persistence=inmemory - -%mysql.quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/shop -%mysql.quarkus.datasource.username=root -%mysql.quarkus.datasource.password=test -%mysql.quarkus.hibernate-orm.database.generation=update -%mysql.persistence=mysql +spring.datasource.url=jdbc:h2:mem:testdb +persistence=inmemory diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java index ba7657f..111b362 100644 --- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java +++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/CartTest.java @@ -4,34 +4,38 @@ import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.LED_LIGHTS; import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.MONITOR_DESK_MOUNT; import static io.restassured.RestAssured.given; -import static jakarta.ws.rs.core.Response.Status.NO_CONTENT; +import static org.springframework.http.HttpStatus.NO_CONTENT; -import eu.happycoders.shop.adapter.TestProfileWithMySQL; import eu.happycoders.shop.model.cart.Cart; import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException; import eu.happycoders.shop.model.customer.CustomerId; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; import io.restassured.response.Response; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest -@TestProfile(TestProfileWithMySQL.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test-with-mysql") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CartTest { private static final CustomerId TEST_CUSTOMER_ID = new CustomerId(61157); private static final String CARTS_PATH = "/carts/" + TEST_CUSTOMER_ID.value(); + @LocalServerPort private Integer port; + @Test @Order(1) void givenAnEmptyCart_addLineItem_addsTheLineItemAndReturnsTheCartWithTheAddedItem() throws NotEnoughItemsInStockException { Response response = given() + .port(port) .queryParam("productId", LED_LIGHTS.id().value()) .queryParam("quantity", 3) .post(CARTS_PATH + "/line-items") @@ -51,6 +55,7 @@ void givenACartWithOneLineItem_addLineItem_addsTheLineItemAndReturnsACartWithTwo throws NotEnoughItemsInStockException { Response response = given() + .port(port) .queryParam("productId", MONITOR_DESK_MOUNT.id().value()) .queryParam("quantity", 1) .post(CARTS_PATH + "/line-items") @@ -68,7 +73,7 @@ void givenACartWithOneLineItem_addLineItem_addsTheLineItemAndReturnsACartWithTwo @Test @Order(3) void givenACartWithTwoLineItems_getCart_returnsTheCart() throws NotEnoughItemsInStockException { - Response response = given().get(CARTS_PATH).then().extract().response(); + Response response = given().port(port).get(CARTS_PATH).then().extract().response(); Cart expectedCart = new Cart(TEST_CUSTOMER_ID); expectedCart.addProduct(LED_LIGHTS, 3); @@ -80,13 +85,13 @@ void givenACartWithTwoLineItems_getCart_returnsTheCart() throws NotEnoughItemsIn @Test @Order(4) void givenACartWithTwoLineItems_delete_returnsStatusCodeNoContent() { - given().delete(CARTS_PATH).then().statusCode(NO_CONTENT.getStatusCode()); + given().port(port).delete(CARTS_PATH).then().statusCode(NO_CONTENT.value()); } @Test @Order(5) void givenAnEmptiedCart_getCart_returnsAnEmptyCart() { - Response response = given().get(CARTS_PATH).then().extract().response(); + Response response = given().port(port).get(CARTS_PATH).then().extract().response(); assertThatResponseIsCart(response, new Cart(TEST_CUSTOMER_ID)); } diff --git a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java index 2410dea..3a152b7 100644 --- a/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java +++ b/bootstrap/src/test/java/eu/happycoders/shop/bootstrap/e2e/FindProductsTest.java @@ -5,23 +5,26 @@ import static eu.happycoders.shop.adapter.out.persistence.DemoProducts.MONITOR_DESK_MOUNT; import static io.restassured.RestAssured.given; -import eu.happycoders.shop.adapter.TestProfileWithMySQL; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; import io.restassured.response.Response; import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; -@QuarkusTest -@TestProfile(TestProfileWithMySQL.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test-with-mysql") class FindProductsTest { + @LocalServerPort private Integer port; + @Test void givenTestProductsAndAQuery_findProducts_returnsMatchingProducts() { String query = "monitor"; Response response = - given().queryParam("query", query).get("/products").then().extract().response(); + given().port(port).queryParam("query", query).get("/products").then().extract().response(); assertThatResponseIsProductList(response, List.of(COMPUTER_MONITOR, MONITOR_DESK_MOUNT)); } From 6bb98d59554308fc787aa98aa2ffce044638984d Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Thu, 16 Nov 2023 13:46:02 +0100 Subject: [PATCH 6/8] Migrate to Spring Boot - adapt README --- README.md | 21 +++++++++------------ doc/sample-requests.http | 6 +++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 09746b3..067a0f8 100644 --- a/README.md +++ b/README.md @@ -47,29 +47,26 @@ The following diagram shows the hexagonal architecture of the application along ![Hexagonal Architecture Modules](doc/hexagonal-architecture-modules.png) -The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon. +The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon. # How to Run the Application -You can run the application in Quarkus dev mode with the following command: +The easiest way to run the application is to start the `main` method of the `Launcher` class (you'll find it in the `boostrap` module) from your IDE. -```shell -mvn test-compile quarkus:dev -``` +By default, the application will run with the in-memory persistence option. -You can use one of the following VM options to select a persistence mechanism: +To select the MySQL persistence option, start it with the following VM option: -* `-Dpersistence=inmemory` to select the in-memory persistence option (default) -* `-Dpersistence=mysql` to select the MySQL option +`-Dspring.profiles.active=mysql` -For example, to run the application in MySQL mode, enter: +If you selected the MySQL option, you will need a running MySQL database. The easiest way to start one is to use the following Docker command: ```shell -mvn test-compile quarkus:dev -Dpersistence=mysql +docker run --name hexagon-mysql -d -p3306:3306 \ + -e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=test mysql:8.1 ``` -In dev mode, Quarkus will automatically start a MySQL database using Docker, -and it will automatically create all database tables. +The connection parameters for the database are hardcoded in `application-mysql.properties`. If you are using the Docker container as described above, you can leave the connection parameters as they are. Otherwise, you may need to adjust them. # Example Curl Commands diff --git a/doc/sample-requests.http b/doc/sample-requests.http index 5208fea..2c1a5e8 100644 --- a/doc/sample-requests.http +++ b/doc/sample-requests.http @@ -1,11 +1,11 @@ ### Search for products containing "plastic" -GET http://localhost:8080/products/?query=plastic +GET http://localhost:8080/products?query=plastic ### Search for products containing "monitor" -GET http://localhost:8080/products/?query=monitor +GET http://localhost:8080/products?query=monitor ### Invalid search (search query too short) -GET http://localhost:8080/products/?query=x +GET http://localhost:8080/products?query=x ### Get cart GET http://localhost:8080/carts/61157 From d35f8471b50f2e7dc14b6429cc9f92eb814c957a Mon Sep 17 00:00:00 2001 From: Sven Woltmann Date: Wed, 27 Dec 2023 13:56:24 +0000 Subject: [PATCH 7/8] Add part 5 to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 067a0f8..a43bd0d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ It is part of the HappyCoders tutorial series on Hexagonal Architecture: * [Part 2: Hexagonal Architecture with Java - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-java/). * [Part 3: Ports and Adapters Java Tutorial: Adding a Database Adapter](https://www.happycoders.eu/software-craftsmanship/ports-and-adapters-java-tutorial-db/). * [Part 4: Hexagonal Architecture with Quarkus - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-quarkus/). +* [Part 5: Hexagonal Architecture with Spring Boot - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-spring-boot/). # Branches @@ -33,7 +34,7 @@ In the `with-quarkus` branch, you'll find an implementation using [Quarkus](http ## `with-spring` -There will soon be an additional branch with an implementation using [Spring](https://spring.io/) instead of Quarkus. +In the `with-quarkus` branch, you'll find an implementation using [Spring](https://spring.io/) as application framework. # Architecture Overview From 1b9c4d48198ca48204d71de77d7d0dda85443f5a Mon Sep 17 00:00:00 2001 From: Alfredo Rueda Unsain Date: Mon, 28 Oct 2024 20:07:23 +0100 Subject: [PATCH 8/8] Update README.md Little fix in documentation --> In the `with-spring` branch Little fix in documentation --> In the `with-spring` branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a43bd0d..3c4afbb 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ In the `with-quarkus` branch, you'll find an implementation using [Quarkus](http ## `with-spring` -In the `with-quarkus` branch, you'll find an implementation using [Spring](https://spring.io/) as application framework. +In the `with-spring` branch, you'll find an implementation using [Spring](https://spring.io/) as application framework. # Architecture Overview