├── .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 |
--------------------------------------------------------------------------------