├── .gitignore ├── README.md ├── domain ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── rdelgatte │ │ └── hexagonal │ │ ├── price │ │ ├── api │ │ │ ├── PriceService.java │ │ │ └── PriceServiceImpl.java │ │ ├── domain │ │ │ └── Price.java │ │ └── spi │ │ │ └── PriceRepository.java │ │ └── product │ │ ├── api │ │ ├── ProductService.java │ │ └── ProductServiceImpl.java │ │ ├── domain │ │ └── Product.java │ │ └── spi │ │ └── ProductRepository.java │ └── test │ └── java │ └── com │ └── rdelgatte │ └── hexagonal │ ├── price │ └── api │ │ └── PriceServiceImplTest.java │ └── product │ └── api │ └── ProductServiceImplTest.java ├── infrastructure ├── memory-persistence │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── rdelgatte │ │ │ └── hexagonal │ │ │ └── persistence │ │ │ └── inmemory │ │ │ └── repository │ │ │ ├── InMemoryPriceRepository.java │ │ │ └── InMemoryProductRepository.java │ │ └── test │ │ └── java │ │ └── com │ │ └── rdelgatte │ │ └── hexagonal │ │ └── persistence │ │ └── inmemory │ │ └── repository │ │ ├── InMemoryPriceRepositoryTest.java │ │ └── InMemoryProductRepositoryTest.java ├── mysql-persistence │ ├── docker-compose.yml │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── rdelgatte │ │ │ └── hexagonal │ │ │ └── persistence │ │ │ └── mysql │ │ │ ├── MysqlPersistenceApplication.java │ │ │ ├── model │ │ │ └── MysqlProduct.java │ │ │ └── repository │ │ │ ├── MysqlProductRepositoryImpl.java │ │ │ └── ProductRepository.java │ │ └── resources │ │ └── application.yml ├── pom.xml └── rest-client │ ├── pom.xml │ └── src │ ├── main │ └── java │ │ └── com │ │ └── rdelgatte │ │ └── hexagonal │ │ └── client │ │ └── rest │ │ ├── Application.java │ │ ├── configuration │ │ └── ApplicationConfiguration.java │ │ └── controller │ │ ├── PriceController.java │ │ └── ProductController.java │ └── test │ └── java │ └── com │ └── rdelgatte │ └── hexagonal │ └── client │ └── rest │ └── controller │ ├── PriceControllerTest.java │ └── ProductControllerTest.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | **.iml 2 | .idea 3 | domain/target 4 | infrastructure/memory-persistence/target 5 | infrastructure/mysql-persistence/target 6 | infrastructure/rest-client/target 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hexagonal architecture with Spring 2 | 3 | Some references about hexagonal architecture / DDD (Domain Driven Design): 4 | - https://www.youtube.com/watch?v=th4AgBcrEHA (introduction by Alistair Cockburn) 5 | - https://www.youtube.com/watch?v=Hi5aDfRe-aE (préz devoxx) 6 | - https://en.wikipedia.org/wiki/Domain-driven_design 7 | - https://blog.xebia.fr/2016/03/16/perennisez-votre-metier-avec-larchitecture-hexagonale/ 8 | - https://blog.octo.com/architecture-hexagonale-trois-principes-et-un-exemple-dimplementation/ 9 | 10 | ## Start-up 11 | 12 | - Run `docker-compose up -d` in infrastructure/mysql-persistence to start dockerized mysql DB 13 | - Start `rest-client` application 14 | 15 | #### Products 16 | 17 | - Create a product: `curl --header "Content-Type: application/json" --request POST --data '{"code": "1234","label": "My awesome product"}' http://localhost:8080/products` 18 | - List products: `curl --header "Content-Type: application/json" --request GET http://localhost:8080/products` 19 | - Find product: `curl --header "Content-Type: application/json" --request GET http://localhost:8080/products/1234` 20 | 21 | If you try to run the create product statement twice with the same product code, you will get an exception handled by domain: 22 | ``` 23 | {"timestamp":"2018-10-30T09:55:10.837+0000","status":500,"error":"Internal Server Error","message":"Product 1234 already exists so you can't create it","path":"/products"} 24 | ``` 25 | 26 | #### Prices 27 | 28 | - Create a price: `curl --header "Content-Type: application/json" --request POST --data '{"value": 1234}' http://localhost:8080/prices` 29 | - List prices: `curl --header "Content-Type: application/json" --request GET http://localhost:8080/prices` 30 | 31 | ## Domain 32 | 33 | `domain` is implemented as a standalone Java module which has no dependencies to any framework (neither spring). 34 | 35 | Actually it has only two dependencies so we can use `Lombok` and `Vavr` as libraries to make data manipulation easier. 36 | 37 | ``` 38 | 39 | 40 | 41 | org.projectlombok 42 | lombok 43 | ${lombok.version} 44 | 45 | 46 | io.vavr 47 | vavr 48 | ${vavr.version} 49 | 50 | 51 | ``` 52 | 53 | As defined in hexagonal architecture, in `domain` you will only find the data model definition and *API* + *SPI* interface definitions. 54 | 55 | ## Infrastructure 56 | 57 | Here we will define the interactions over the domain so it implements these *ports* to implements the way to : 58 | - interact with the domain (triggering actions) 59 | - define where the domain gets its resources (persistence) 60 | 61 | To do so, there are multiple sub-modules under infrastructure. 62 | 63 | ## API (Application Provider Interfaces) 64 | 65 | It describes all the ports for everything that needs **to query the domain**. 66 | 67 | These interfaces are implemented by the domain. 68 | 69 | #### Rest client 70 | 71 | This module aims to expose some *rest* entry points to interact with products and prices. 72 | 73 | - Module: `rest-client` 74 | - Parent module: `infrastructure` 75 | - Dependencies: `spring-boot` 76 | 77 | In `ApplicationConfiguration`, we can find the definition of both `ProductService` and `PriceService` adapters from the domain where we can decide which repository we should use (in memory | mysql). 78 | 79 | In the following example (default), we define the **persistence mode** for each service we want to use: 80 | - `ProductService` will use `MysqlProductRepository` (meaning products will be persisted in a mysql DB) 81 | - `PriceService` will use `InMemoryPriceRepository` (meaning prices will be persisted in memory) 82 | 83 | ``` 84 | private MysqlProductRepositoryImpl mysqlProductRepository; 85 | 86 | private InMemoryPriceRepository inMemoryPriceRepository() { 87 | return new InMemoryPriceRepository(); 88 | } 89 | 90 | @Bean 91 | public ProductService productService() { 92 | return new ProductServiceImpl(mysqlProductRepository); 93 | } 94 | 95 | @Bean 96 | public PriceService priceService() { 97 | return new PriceServiceImpl(inMemoryPriceRepository()); 98 | } 99 | ``` 100 | 101 | Note: to use outside package beans like for mysql persistence beans, we need to explicit the package to configuration as below: 102 | ``` 103 | @ComponentScan(basePackages = { 104 | "com.rdelgatte.hexagonal.persistence.mysql", 105 | }) 106 | ``` 107 | 108 | ## SPI 109 | 110 | It gathers all the ports required by the domain **to retrieve information or get some services from third parties**. 111 | 112 | These interfaces are defined in the domain and implemented by the right side of the infrastructure. 113 | 114 | #### In-memory persistence 115 | 116 | Through this implementation, domain data can be persisted in memory by implementing SPI ports for domain `ProductRepository` and `PriceRepository`. 117 | Here is an example: 118 | 119 | ``` 120 | @Data 121 | @AllArgsConstructor 122 | @NoArgsConstructor 123 | @Wither 124 | public class InMemoryProductRepository implements ProductRepository { 125 | 126 | private List inMemoryProducts = List(); 127 | 128 | public Product addProduct(Product product) { 129 | this.inMemoryProducts = getInMemoryProducts().append(product); 130 | return product; 131 | } 132 | 133 | public void deleteProduct(UUID productId) { 134 | this.inMemoryProducts = getInMemoryProducts().filter(product -> !product.getId().equals(productId)); 135 | } 136 | 137 | public Option findProductByCode(String code) { 138 | return getInMemoryProducts().find(product -> product.getCode().equals(code)); 139 | } 140 | 141 | public List findAllProducts() { 142 | return getInMemoryProducts(); 143 | } 144 | } 145 | ``` 146 | 147 | #### Mysql persistence 148 | 149 | Using spring data and mysql connector, this Spring boot project define the persistence of domain data by implementing the same SPI ports as before. 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /domain/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | spring-hexagonal-architecture 9 | com.rdelgatte.hexagonal 10 | 1.0-SNAPSHOT 11 | 12 | 13 | domain 14 | 15 | 16 | 17 | 18 | org.projectlombok 19 | lombok 20 | ${lombok.version} 21 | 22 | 23 | io.vavr 24 | vavr 25 | ${vavr.version} 26 | 27 | 28 | org.assertj 29 | assertj-core 30 | ${assertj.version} 31 | test 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/price/api/PriceService.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.price.api; 2 | 3 | import com.rdelgatte.hexagonal.price.domain.Price; 4 | import io.vavr.collection.List; 5 | 6 | public interface PriceService { 7 | 8 | Price createPrice(Price price); 9 | 10 | List getAllPrices(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/price/api/PriceServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.price.api; 2 | 3 | import com.rdelgatte.hexagonal.price.domain.Price; 4 | import com.rdelgatte.hexagonal.price.spi.PriceRepository; 5 | import io.vavr.collection.List; 6 | import io.vavr.control.Option; 7 | 8 | public class PriceServiceImpl implements PriceService { 9 | 10 | private final PriceRepository priceRepository; 11 | 12 | public PriceServiceImpl(PriceRepository priceRepository) { 13 | this.priceRepository = priceRepository; 14 | } 15 | 16 | public Price createPrice(Price price) { 17 | Option priceById = priceRepository.findPriceById(price.getId()); 18 | if (priceById.isDefined()) { 19 | throw new IllegalArgumentException( 20 | "Price " + price.getId().toString() + " already exists so you can't create it"); 21 | } 22 | return priceRepository.addPrice(price); 23 | } 24 | 25 | public List getAllPrices() { 26 | return priceRepository.findAllPrices(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/price/domain/Price.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.price.domain; 2 | 3 | import java.util.UUID; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.Wither; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Wither 13 | public class Price { 14 | 15 | private UUID id = UUID.randomUUID(); 16 | private int value = 0; 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/price/spi/PriceRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.price.spi; 2 | 3 | import com.rdelgatte.hexagonal.price.domain.Price; 4 | import io.vavr.collection.List; 5 | import io.vavr.control.Option; 6 | import java.util.UUID; 7 | 8 | public interface PriceRepository { 9 | 10 | Price addPrice(Price price); 11 | 12 | void deletePrice(UUID priceId); 13 | 14 | Option findPriceById(UUID priceId); 15 | 16 | List findAllPrices(); 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/product/api/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.product.api; 2 | 3 | import com.rdelgatte.hexagonal.product.domain.Product; 4 | import io.vavr.collection.List; 5 | import io.vavr.control.Option; 6 | 7 | public interface ProductService { 8 | 9 | Product createProduct(Product product); 10 | 11 | void deleteProduct(String code); 12 | 13 | List getAllProducts(); 14 | 15 | Option findProductByCode(String code); 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/product/api/ProductServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.product.api; 2 | 3 | import com.rdelgatte.hexagonal.product.domain.Product; 4 | import com.rdelgatte.hexagonal.product.spi.ProductRepository; 5 | import io.vavr.collection.List; 6 | import io.vavr.control.Option; 7 | import lombok.NonNull; 8 | 9 | public class ProductServiceImpl implements ProductService { 10 | 11 | private final ProductRepository productRepository; 12 | 13 | public ProductServiceImpl(ProductRepository productRepository) { 14 | this.productRepository = productRepository; 15 | } 16 | 17 | public Product createProduct(Product product) { 18 | if (product.getCode().isEmpty()) { 19 | throw new IllegalArgumentException("There is no code for the product"); 20 | } 21 | Option productById = productRepository.findProductByCode(product.getCode()); 22 | if (productById.isDefined()) { 23 | throw new IllegalArgumentException( 24 | "Product " + product.getCode() + " already exists so you can't create it"); 25 | } 26 | return productRepository.addProduct(product); 27 | } 28 | 29 | public void deleteProduct(@NonNull String code) { 30 | Option productByCode = findProductByCode(code); 31 | if (productByCode.isEmpty()) { 32 | throw new IllegalArgumentException("Product " + code + " does not exist"); 33 | } 34 | productRepository.deleteProduct(productByCode.get().getId()); 35 | } 36 | 37 | public List getAllProducts() { 38 | return productRepository.findAllProducts(); 39 | } 40 | 41 | public Option findProductByCode(String code) { 42 | return productRepository.findProductByCode(code); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/product/domain/Product.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.product.domain; 2 | 3 | import java.util.UUID; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.experimental.Wither; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Wither 13 | public class Product { 14 | 15 | private UUID id = UUID.randomUUID(); 16 | private String code = ""; 17 | private String label = ""; 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/java/com/rdelgatte/hexagonal/product/spi/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.product.spi; 2 | 3 | import com.rdelgatte.hexagonal.product.domain.Product; 4 | import io.vavr.collection.List; 5 | import io.vavr.control.Option; 6 | import java.util.UUID; 7 | 8 | public interface ProductRepository { 9 | 10 | Product addProduct(Product product); 11 | 12 | void deleteProduct(UUID productId); 13 | 14 | Option findProductByCode(String code); 15 | 16 | List findAllProducts(); 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/test/java/com/rdelgatte/hexagonal/price/api/PriceServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.price.api; 2 | 3 | import static io.vavr.API.List; 4 | import static io.vavr.API.None; 5 | import static io.vavr.API.Option; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.mockito.Mockito.when; 9 | 10 | import com.rdelgatte.hexagonal.price.domain.Price; 11 | import com.rdelgatte.hexagonal.price.spi.PriceRepository; 12 | import java.util.UUID; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | class PriceServiceImplTest { 21 | 22 | private PriceServiceImpl cut; 23 | @Mock 24 | private PriceRepository priceRepositoryMock; 25 | private Price price; 26 | private UUID priceId; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | cut = new PriceServiceImpl(priceRepositoryMock); 31 | priceId = UUID.randomUUID(); 32 | price = new Price(priceId, 1234); 33 | } 34 | 35 | /** 36 | * {@link PriceServiceImpl#createPrice(Price)} 37 | */ 38 | @Test 39 | void createExistingValidPrice_throwsException() { 40 | when(priceRepositoryMock.findPriceById(priceId)).thenReturn(Option(price)); 41 | 42 | IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, 43 | () -> cut.createPrice(price)); 44 | 45 | assertThat(illegalArgumentException.getMessage()) 46 | .isEqualTo("Price " + price.getId().toString() + " already exists so you can't create it"); 47 | } 48 | 49 | @Test 50 | void createUnknownValidPrice_createPrice() { 51 | when(priceRepositoryMock.findPriceById(priceId)).thenReturn(None()); 52 | when(priceRepositoryMock.addPrice(price)).thenReturn(price); 53 | 54 | assertThat(cut.createPrice(price)).isEqualTo(price); 55 | } 56 | 57 | /** 58 | * {@link PriceServiceImpl#getAllPrices()} 59 | */ 60 | @Test 61 | void getAllPrices_returnPrices() { 62 | when(priceRepositoryMock.findAllPrices()).thenReturn(List(price)); 63 | 64 | assertThat(cut.getAllPrices()).containsExactly(price); 65 | } 66 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/rdelgatte/hexagonal/product/api/ProductServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.product.api; 2 | 3 | import static io.vavr.API.List; 4 | import static io.vavr.API.None; 5 | import static io.vavr.API.Option; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.when; 10 | 11 | import com.rdelgatte.hexagonal.product.domain.Product; 12 | import com.rdelgatte.hexagonal.product.spi.ProductRepository; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | @ExtendWith(MockitoExtension.class) 20 | class ProductServiceImplTest { 21 | 22 | private ProductServiceImpl cut; 23 | @Mock 24 | private ProductRepository productRepositoryMock; 25 | private Product product; 26 | private String productCode = "123"; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | cut = new ProductServiceImpl(productRepositoryMock); 31 | product = new Product() 32 | .withCode(productCode) 33 | .withLabel("My awesome product"); 34 | } 35 | 36 | /** 37 | * {@link ProductServiceImpl#createProduct(Product)} 38 | */ 39 | @Test 40 | void productAlreadyExists_throwsException() { 41 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(Option(product)); 42 | 43 | IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, 44 | () -> cut.createProduct(product)); 45 | assertThat(illegalArgumentException.getMessage()) 46 | .isEqualTo("Product " + product.getCode() + " already exists so you can't create it"); 47 | } 48 | 49 | @Test 50 | void productWithoutCode_throwsException() { 51 | IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, 52 | () -> cut.createProduct(new Product())); 53 | 54 | assertThat(illegalArgumentException.getMessage()).isEqualTo("There is no code for the product"); 55 | } 56 | 57 | @Test 58 | void unknownValidProduct_createProduct() { 59 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(None()); 60 | when(productRepositoryMock.addProduct(product)).thenReturn(product); 61 | 62 | assertThat(cut.createProduct(product)).isEqualTo(product); 63 | } 64 | 65 | /** 66 | * {@link ProductServiceImpl#findProductByCode(String)} 67 | */ 68 | @Test 69 | void unknownProduct_returnsNone() { 70 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(None()); 71 | 72 | assertThat(cut.findProductByCode(productCode)).isEqualTo(None()); 73 | } 74 | 75 | @Test 76 | void existingProduct_returnsProduct() { 77 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(Option(product)); 78 | 79 | assertThat(cut.findProductByCode(productCode)).isEqualTo(Option(product)); 80 | } 81 | 82 | /** 83 | * {@link ProductServiceImpl#deleteProduct(String)} 84 | */ 85 | @Test 86 | void deleteUnknownProduct_throwsException() { 87 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(None()); 88 | 89 | IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, 90 | () -> cut.deleteProduct(productCode)); 91 | assertThat(illegalArgumentException.getMessage()).isEqualTo("Product 123 does not exist"); 92 | } 93 | 94 | @Test 95 | void deleteNoCode_throwsException() { 96 | assertThrows(NullPointerException.class, () -> cut.deleteProduct(null)); 97 | } 98 | 99 | @Test 100 | void deleteExistingProduct_deleteProduct() { 101 | when(productRepositoryMock.findProductByCode(productCode)).thenReturn(Option(product)); 102 | 103 | cut.deleteProduct(productCode); 104 | 105 | verify(productRepositoryMock).deleteProduct(product.getId()); 106 | } 107 | 108 | /** 109 | * {@link ProductServiceImpl#getAllProducts()} 110 | */ 111 | @Test 112 | void getAllProducts_returnProducts() { 113 | when(productRepositoryMock.findAllProducts()).thenReturn(List(product)); 114 | 115 | assertThat(cut.getAllProducts()).containsExactly(product); 116 | } 117 | } -------------------------------------------------------------------------------- /infrastructure/memory-persistence/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | infrastructure 9 | com.rdelgatte.hexagonal 10 | 1.0-SNAPSHOT 11 | ../pom.xml 12 | 13 | 14 | memory-persistence 15 | 1.0-SNAPSHOT 16 | memory-persistence 17 | Persistence layer for Hexagonal based on memory 18 | 19 | 20 | 3.7.0 21 | 22 | 23 | 24 | 25 | org.assertj 26 | assertj-core 27 | ${assertj.version} 28 | test 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-compiler-plugin 37 | ${maven-compiler-plugin.version} 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /infrastructure/memory-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/inmemory/repository/InMemoryPriceRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.inmemory.repository; 2 | 3 | import static io.vavr.API.List; 4 | 5 | import com.rdelgatte.hexagonal.price.domain.Price; 6 | import com.rdelgatte.hexagonal.price.spi.PriceRepository; 7 | import io.vavr.collection.List; 8 | import io.vavr.control.Option; 9 | import java.util.UUID; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | import lombok.experimental.Wither; 14 | 15 | @Data 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Wither 19 | public class InMemoryPriceRepository implements PriceRepository { 20 | 21 | private List inMemoryPrices = List(); 22 | 23 | public Price addPrice(Price price) { 24 | this.inMemoryPrices = getInMemoryPrices().append(price); 25 | return price; 26 | } 27 | 28 | public void deletePrice(UUID priceId) { 29 | this.inMemoryPrices = getInMemoryPrices().filter(price -> !price.getId().equals(priceId)); 30 | } 31 | 32 | public Option findPriceById(UUID priceId) { 33 | return getInMemoryPrices().find(price -> price.getId().equals(priceId)); 34 | } 35 | 36 | public List findAllPrices() { 37 | return getInMemoryPrices(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /infrastructure/memory-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/inmemory/repository/InMemoryProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.inmemory.repository; 2 | 3 | import static io.vavr.API.List; 4 | 5 | import com.rdelgatte.hexagonal.product.domain.Product; 6 | import com.rdelgatte.hexagonal.product.spi.ProductRepository; 7 | import io.vavr.collection.List; 8 | import io.vavr.control.Option; 9 | import java.util.UUID; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Data; 12 | import lombok.NoArgsConstructor; 13 | import lombok.experimental.Wither; 14 | 15 | @Data 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Wither 19 | public class InMemoryProductRepository implements ProductRepository { 20 | 21 | private List inMemoryProducts = List(); 22 | 23 | public Product addProduct(Product product) { 24 | this.inMemoryProducts = getInMemoryProducts().append(product); 25 | return product; 26 | } 27 | 28 | public void deleteProduct(UUID productId) { 29 | this.inMemoryProducts = getInMemoryProducts().filter(product -> !product.getId().equals(productId)); 30 | } 31 | 32 | public Option findProductByCode(String code) { 33 | return getInMemoryProducts().find(product -> product.getCode().equals(code)); 34 | } 35 | 36 | public List findAllProducts() { 37 | return getInMemoryProducts(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /infrastructure/memory-persistence/src/test/java/com/rdelgatte/hexagonal/persistence/inmemory/repository/InMemoryPriceRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.inmemory.repository; 2 | 3 | import static io.vavr.API.None; 4 | import static io.vavr.API.Option; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import com.rdelgatte.hexagonal.price.domain.Price; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class InMemoryPriceRepositoryTest { 13 | 14 | private InMemoryPriceRepository cut; 15 | private Price price; 16 | 17 | @BeforeEach 18 | void setUp() { 19 | cut = new InMemoryPriceRepository(); 20 | price = new Price(UUID.randomUUID(), 1234); 21 | } 22 | 23 | /** 24 | * {@link InMemoryPriceRepository#addPrice(Price)} 25 | */ 26 | @Test 27 | void addPriceAndAssert() { 28 | assertThat(cut.addPrice(price)).isEqualTo(price); 29 | 30 | assertThat(cut.findAllPrices()).containsExactly(price); 31 | } 32 | 33 | /** 34 | * {@link InMemoryPriceRepository#deletePrice(UUID)} 35 | */ 36 | @Test 37 | void deleteExistingPrice() { 38 | cut.addPrice(price); 39 | cut.deletePrice(price.getId()); 40 | 41 | assertThat(cut.findPriceById(price.getId())).isEmpty(); 42 | } 43 | 44 | /** 45 | * {@link InMemoryPriceRepository#findPriceById(UUID)} 46 | */ 47 | @Test 48 | void findExistingPrice() { 49 | cut.addPrice(price); 50 | 51 | assertThat(cut.findPriceById(price.getId())).isEqualTo(Option(price)); 52 | } 53 | 54 | @Test 55 | void findUnknownPrice() { 56 | assertThat(cut.findPriceById(price.getId())).isEqualTo(None()); 57 | } 58 | } -------------------------------------------------------------------------------- /infrastructure/memory-persistence/src/test/java/com/rdelgatte/hexagonal/persistence/inmemory/repository/InMemoryProductRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.inmemory.repository; 2 | 3 | import static io.vavr.API.List; 4 | import static io.vavr.API.None; 5 | import static io.vavr.API.Option; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | import com.rdelgatte.hexagonal.product.domain.Product; 9 | import java.util.UUID; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class InMemoryProductRepositoryTest { 14 | 15 | private InMemoryProductRepository cut; 16 | private Product product; 17 | 18 | @BeforeEach 19 | void setUp() { 20 | cut = new InMemoryProductRepository(); 21 | product = new Product() 22 | .withCode("1234") 23 | .withLabel("My awesome product"); 24 | } 25 | 26 | /** 27 | * {@link InMemoryProductRepository#addProduct(Product)} 28 | */ 29 | @Test 30 | void addProductAndAssert() { 31 | assertThat(cut.addProduct(product)).isEqualTo(product); 32 | assertThat(cut.getInMemoryProducts()).isEqualTo(List(product)); 33 | } 34 | 35 | /** 36 | * {@link InMemoryProductRepository#deleteProduct(UUID)} 37 | */ 38 | @Test 39 | void deleteExistingProduct() { 40 | cut.addProduct(product); 41 | cut.deleteProduct(product.getId()); 42 | 43 | assertThat(cut.findProductByCode(product.getCode())).isEmpty(); 44 | } 45 | 46 | /** 47 | * {@link InMemoryProductRepository#findProductByCode(String)} 48 | */ 49 | @Test 50 | void findExistingProduct() { 51 | cut.addProduct(product); 52 | 53 | assertThat(cut.findProductByCode(product.getCode())).isEqualTo(Option(product)); 54 | } 55 | 56 | @Test 57 | void findUnknownProduct() { 58 | assertThat(cut.findProductByCode(product.getCode())).isEqualTo(None()); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | database: 5 | image: mysql:5.7 6 | environment: 7 | - MYSQL_USER=hexagonal 8 | - MYSQL_PASSWORD=hexagonal 9 | - MYSQL_ROOT_PASSWORD=hexagonal 10 | - MYSQL_DATABASE=hexagonal 11 | ports: 12 | - "3306:3306" 13 | 14 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.rdelgatte.hexagonal 8 | infrastructure 9 | 1.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | 13 | mysql-persistence 14 | 1.0-SNAPSHOT 15 | mysql-persistence 16 | Persistence layer for Hexagonal based on Mysql 17 | 18 | 19 | 5.1.42 20 | 21 | 22 | 23 | 24 | mysql 25 | mysql-connector-java 26 | ${mysql-connector-java.version} 27 | runtime 28 | 29 | 30 | org.projectlombok 31 | lombok 32 | ${lombok.version} 33 | compile 34 | 35 | 36 | org.springframework.data 37 | spring-data-commons 38 | compile 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-jpa 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/mysql/MysqlPersistenceApplication.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.mysql; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MysqlPersistenceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(MysqlPersistenceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/mysql/model/MysqlProduct.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.mysql.model; 2 | 3 | import java.util.UUID; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Id; 6 | import javax.persistence.Table; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Entity 12 | @Table(name = "product") 13 | @Data 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class MysqlProduct { 17 | 18 | @Id 19 | private UUID id = UUID.randomUUID(); 20 | private String code = ""; 21 | private String label = ""; 22 | 23 | public MysqlProduct fromDomain(com.rdelgatte.hexagonal.product.domain.Product product) { 24 | return new MysqlProduct(product.getId(), product.getCode(), product.getLabel()); 25 | } 26 | 27 | public com.rdelgatte.hexagonal.product.domain.Product toDomain() { 28 | return new com.rdelgatte.hexagonal.product.domain.Product(id, code, label); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/mysql/repository/MysqlProductRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.mysql.repository; 2 | 3 | import com.rdelgatte.hexagonal.persistence.mysql.model.MysqlProduct; 4 | import com.rdelgatte.hexagonal.product.spi.ProductRepository; 5 | import io.vavr.collection.List; 6 | import io.vavr.control.Option; 7 | import java.util.UUID; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Repository 13 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 14 | public class MysqlProductRepositoryImpl implements ProductRepository { 15 | 16 | private com.rdelgatte.hexagonal.persistence.mysql.repository.ProductRepository productRepository; 17 | 18 | @Override 19 | public com.rdelgatte.hexagonal.product.domain.Product addProduct( 20 | com.rdelgatte.hexagonal.product.domain.Product product) { 21 | productRepository.save(new MysqlProduct().fromDomain(product)); 22 | return product; 23 | } 24 | 25 | @Override 26 | public void deleteProduct(UUID productId) { 27 | productRepository.deleteById(productId); 28 | } 29 | 30 | @Override 31 | public Option findProductByCode(String code) { 32 | return productRepository.findOneByCode(code).map(MysqlProduct::toDomain); 33 | } 34 | 35 | @Override 36 | public List findAllProducts() { 37 | return List.ofAll(productRepository.findAll()) 38 | .map(MysqlProduct::toDomain); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/src/main/java/com/rdelgatte/hexagonal/persistence/mysql/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.persistence.mysql.repository; 2 | 3 | import com.rdelgatte.hexagonal.persistence.mysql.model.MysqlProduct; 4 | import io.vavr.control.Option; 5 | import java.util.UUID; 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | interface ProductRepository extends CrudRepository { 9 | 10 | Option findOneByCode(String code); 11 | } 12 | -------------------------------------------------------------------------------- /infrastructure/mysql-persistence/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | hibernate: 4 | ddl-auto: update 5 | datasource: 6 | url: jdbc:mysql://localhost:3306/hexagonal?useSSL=false 7 | username: hexagonal 8 | password: hexagonal -------------------------------------------------------------------------------- /infrastructure/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | spring-hexagonal-architecture 9 | com.rdelgatte.hexagonal 10 | 1.0-SNAPSHOT 11 | ../pom.xml 12 | 13 | 14 | infrastructure 15 | 16 | pom 17 | 18 | memory-persistence 19 | mysql-persistence 20 | rest-client 21 | 22 | 23 | 24 | 25 | com.rdelgatte.hexagonal 26 | domain 27 | 1.0-SNAPSHOT 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /infrastructure/rest-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | infrastructure 9 | com.rdelgatte.hexagonal 10 | 1.0-SNAPSHOT 11 | ../pom.xml 12 | 13 | 14 | rest-client 15 | 1.0-SNAPSHOT 16 | rest-client 17 | Client layer for Hexagonal based on rest endpoints 18 | 19 | 20 | 21 | com.rdelgatte.hexagonal 22 | memory-persistence 23 | 1.0-SNAPSHOT 24 | 25 | 26 | com.rdelgatte.hexagonal 27 | mysql-persistence 28 | 1.0-SNAPSHOT 29 | compile 30 | 31 | 32 | 33 | 34 | org.assertj 35 | assertj-core 36 | ${assertj.version} 37 | test 38 | 39 | 40 | io.vavr 41 | vavr-jackson 42 | ${vavr.version} 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-dependencies 51 | ${spring-boot.version} 52 | pom 53 | import 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-data-rest 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-starter-test 63 | test 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | ${spring-boot.version} 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /infrastructure/rest-client/src/main/java/com/rdelgatte/hexagonal/client/rest/Application.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest; 2 | 3 | 4 | import com.fasterxml.jackson.databind.Module; 5 | import io.vavr.jackson.datatype.VavrModule; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | /** 11 | * Main application 12 | */ 13 | @SpringBootApplication 14 | public class Application { 15 | 16 | public static void main(String[] args) { 17 | // Sonar complains that this context is never closed, however it implements AutoClosable (see https://jira.sonarsource.com/browse/SONARJAVA-1687) 18 | SpringApplication.run(Application.class, args); // NOSONAR 19 | } 20 | 21 | @Bean 22 | public Module vavrFriendlyJackson() { 23 | return new VavrModule(); 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /infrastructure/rest-client/src/main/java/com/rdelgatte/hexagonal/client/rest/configuration/ApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest.configuration; 2 | 3 | import com.rdelgatte.hexagonal.persistence.inmemory.repository.InMemoryPriceRepository; 4 | import com.rdelgatte.hexagonal.persistence.mysql.repository.MysqlProductRepositoryImpl; 5 | import com.rdelgatte.hexagonal.price.api.PriceService; 6 | import com.rdelgatte.hexagonal.price.api.PriceServiceImpl; 7 | import com.rdelgatte.hexagonal.product.api.ProductService; 8 | import com.rdelgatte.hexagonal.product.api.ProductServiceImpl; 9 | import lombok.AllArgsConstructor; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.ComponentScan; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | @Configuration 16 | @AllArgsConstructor(onConstructor = @__(@Autowired)) 17 | // We need to provide the explicit packages path to scan for infrastructure layer autowiring 18 | @ComponentScan(basePackages = { 19 | "com.rdelgatte.hexagonal.persistence.mysql", 20 | }) 21 | public class ApplicationConfiguration { 22 | 23 | private MysqlProductRepositoryImpl mysqlProductRepository; 24 | 25 | private InMemoryPriceRepository inMemoryPriceRepository() { 26 | return new InMemoryPriceRepository(); 27 | } 28 | 29 | @Bean 30 | public ProductService productService() { 31 | return new ProductServiceImpl(mysqlProductRepository); 32 | } 33 | 34 | @Bean 35 | public PriceService priceService() { 36 | return new PriceServiceImpl(inMemoryPriceRepository()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /infrastructure/rest-client/src/main/java/com/rdelgatte/hexagonal/client/rest/controller/PriceController.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest.controller; 2 | 3 | import com.rdelgatte.hexagonal.price.api.PriceService; 4 | import com.rdelgatte.hexagonal.price.domain.Price; 5 | import io.vavr.collection.List; 6 | import lombok.AllArgsConstructor; 7 | import lombok.NonNull; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | 14 | @RestController 15 | @RequestMapping(PriceController.BASE_PATH) 16 | @AllArgsConstructor 17 | class PriceController { 18 | 19 | static final String BASE_PATH = "prices"; 20 | private final PriceService priceService; 21 | 22 | @PostMapping 23 | void createPrice(@RequestBody @NonNull Price price) { 24 | priceService.createPrice(price); 25 | } 26 | 27 | @GetMapping 28 | List findAll() { 29 | return priceService.getAllPrices(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /infrastructure/rest-client/src/main/java/com/rdelgatte/hexagonal/client/rest/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest.controller; 2 | 3 | import com.rdelgatte.hexagonal.product.api.ProductService; 4 | import com.rdelgatte.hexagonal.product.domain.Product; 5 | import io.vavr.collection.List; 6 | import io.vavr.control.Option; 7 | import lombok.AllArgsConstructor; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.DeleteMapping; 15 | 16 | @RestController 17 | @RequestMapping(ProductController.BASE_PATH) 18 | @AllArgsConstructor 19 | public 20 | class ProductController { 21 | 22 | static final String BASE_PATH = "products"; 23 | private static final String RESOURCE_PATH = "{code}"; 24 | private final ProductService productService; 25 | 26 | @PostMapping 27 | void createProduct(@RequestBody Product product) { 28 | productService.createProduct(product); 29 | } 30 | 31 | @GetMapping 32 | List findAll() { 33 | return productService.getAllProducts(); 34 | } 35 | 36 | @GetMapping(RESOURCE_PATH) 37 | Option find(@PathVariable String code) { 38 | return productService.findProductByCode(code); 39 | } 40 | 41 | @DeleteMapping(RESOURCE_PATH) 42 | void delete(@PathVariable String code) { 43 | productService.deleteProduct(code); 44 | } 45 | 46 | 47 | } 48 | -------------------------------------------------------------------------------- /infrastructure/rest-client/src/test/java/com/rdelgatte/hexagonal/client/rest/controller/PriceControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest.controller; 2 | 3 | import static io.vavr.API.List; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.rdelgatte.hexagonal.price.api.PriceService; 9 | import com.rdelgatte.hexagonal.price.domain.Price; 10 | import java.util.UUID; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | class PriceControllerTest { 19 | 20 | private PriceController cut; 21 | @Mock 22 | private PriceService priceServiceMock; 23 | private Price price; 24 | 25 | @BeforeEach 26 | void setUp() { 27 | cut = new PriceController(priceServiceMock); 28 | price = new Price(UUID.randomUUID(), 1234); 29 | } 30 | 31 | /** 32 | * {@link PriceController#createPrice(Price)} 33 | */ 34 | @Test 35 | void createPrice() { 36 | cut.createPrice(price); 37 | 38 | verify(priceServiceMock).createPrice(price); 39 | } 40 | 41 | /** 42 | * {@link PriceController#findAll()} 43 | */ 44 | @Test 45 | void findAllPrices() { 46 | when(priceServiceMock.getAllPrices()).thenReturn(List(price)); 47 | 48 | assertThat(cut.findAll()).containsExactly(price); 49 | } 50 | } -------------------------------------------------------------------------------- /infrastructure/rest-client/src/test/java/com/rdelgatte/hexagonal/client/rest/controller/ProductControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.rdelgatte.hexagonal.client.rest.controller; 2 | 3 | 4 | import com.rdelgatte.hexagonal.product.api.ProductService; 5 | import com.rdelgatte.hexagonal.product.domain.Product; 6 | import io.vavr.control.Option; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.UUID; 14 | 15 | import static io.vavr.API.List; 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.mockito.Mockito.verify; 18 | import static org.mockito.Mockito.when; 19 | 20 | import static io.vavr.API.Option; 21 | import static io.vavr.API.None; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | class ProductControllerTest { 25 | 26 | private Product product; 27 | private ProductController cut; 28 | @Mock 29 | private ProductService productServiceMock; 30 | 31 | @BeforeEach 32 | void setUp() { 33 | cut = new ProductController(productServiceMock); 34 | product = new Product(UUID.randomUUID(), "1616", "Easybreath"); 35 | } 36 | 37 | /** 38 | * {@link ProductController#createProduct(Product)} 39 | */ 40 | @Test 41 | void createProduct() { 42 | when(productServiceMock.createProduct(product)).thenReturn(product); 43 | 44 | cut.createProduct(product); 45 | verify(productServiceMock).createProduct(product); 46 | } 47 | 48 | /** 49 | * {@link ProductController#findAll()} 50 | */ 51 | @Test 52 | void findAllProducts() { 53 | when(productServiceMock.getAllProducts()).thenReturn(List(product)); 54 | 55 | assertThat(cut.findAll()).containsExactly(product); 56 | } 57 | 58 | /** 59 | * {@link ProductController#find(String)} 60 | */ 61 | @Test 62 | void findProductByCode() { 63 | String productCode = "1616"; 64 | when(productServiceMock.findProductByCode(productCode)).thenReturn(Option(product)); 65 | 66 | Option actual = cut.find(productCode); 67 | assertThat(actual).isEqualTo(Option(product)); 68 | } 69 | 70 | void findExistingProduct() { 71 | when(productServiceMock.findProductByCode(product.getCode())).thenReturn(Option(product)); 72 | 73 | assertThat(cut.find(product.getCode())).isEqualTo(Option(product)); 74 | } 75 | 76 | @Test 77 | void findUnknownProduct() { 78 | when(productServiceMock.findProductByCode(product.getCode())).thenReturn(None()); 79 | 80 | assertThat(cut.find(product.getCode())).isEqualTo(None()); 81 | } 82 | 83 | /** 84 | * {@link ProductController#delete(String)} 85 | */ 86 | @Test 87 | void deleteProduct() { 88 | cut.delete(product.getCode()); 89 | 90 | verify(productServiceMock).deleteProduct(product.getCode()); 91 | } 92 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | 9 | org.springframework.boot 10 | spring-boot-starter-parent 11 | 2.1.0.RELEASE 12 | 13 | 14 | com.rdelgatte.hexagonal 15 | spring-hexagonal-architecture 16 | pom 17 | 1.0-SNAPSHOT 18 | 19 | domain 20 | infrastructure 21 | 22 | 23 | 24 | 11 25 | 11 26 | 11 27 | UTF-8 28 | 1.3.1 29 | 5.3.1 30 | 2.22.1 31 | 2.23.0 32 | 1.18.4 33 | 0.9.2 34 | 3.11.1 35 | 2.1.0.RELEASE 36 | 37 | 38 | 39 | 40 | 41 | org.junit.platform 42 | junit-platform-launcher 43 | ${junit-platform.version} 44 | test 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter-engine 49 | ${junit-jupiter.version} 50 | test 51 | 52 | 53 | 54 | org.mockito 55 | mockito-core 56 | ${mockito.version} 57 | test 58 | 59 | 60 | org.mockito 61 | mockito-junit-jupiter 62 | ${mockito.version} 63 | test 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-web 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.surefire 77 | maven-surefire-plugin 78 | ${maven.surefire.version} 79 | 80 | 81 | 82 | 83 | 84 | --------------------------------------------------------------------------------