getCoffeeTypes() {
32 | return coffeeTypes;
33 | }
34 |
35 | @Override
36 | public String toString() {
37 | return "Origin{" +
38 | "name='" + name + '\'' +
39 | ", coffeeTypes=" + coffeeTypes +
40 | '}';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/java/com/sebastian_daschner/coffee_shop/orders/entity/ValidOrder.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.entity;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderValidator;
4 |
5 | import jakarta.validation.Constraint;
6 | import jakarta.validation.Payload;
7 | import jakarta.validation.constraints.NotNull;
8 | import java.lang.annotation.Documented;
9 | import java.lang.annotation.Retention;
10 | import java.lang.annotation.Target;
11 |
12 | import static java.lang.annotation.ElementType.*;
13 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
14 |
15 | @Target({FIELD, METHOD, ANNOTATION_TYPE, PARAMETER})
16 | @Retention(RUNTIME)
17 | @NotNull
18 | @Constraint(validatedBy = OrderValidator.class)
19 | @Documented
20 | public @interface ValidOrder {
21 |
22 | String message() default "";
23 |
24 | Class>[] groups() default {};
25 |
26 | Class extends Payload>[] payload() default {};
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/META-INF/resources/form.js:
--------------------------------------------------------------------------------
1 | const typeSelect = document.querySelector('select[name=type]');
2 | const originSelect = document.querySelector('select[name=origin]');
3 | const submitButton = document.querySelector('button[type=submit]');
4 |
5 | function init() {
6 | typeSelect.addEventListener('change', ev => {
7 | const type = ev.target.value;
8 | if (type) updateOrigins(type);
9 | });
10 | }
11 |
12 | function updateOrigins(type) {
13 | fetch(`${window.location.origin}/types`)
14 | .then(res => res.json())
15 | .then(json => {
16 | const url = json.filter(t => t.type === type)
17 | .map(t => t['_links']['origins']);
18 | fetch(url)
19 | .then(res => res.json())
20 | .then(json => {
21 | originSelect.querySelectorAll('option').forEach(el => {
22 | if (el.value) el.remove();
23 | });
24 | originSelect.removeAttribute('disabled');
25 | submitButton.removeAttribute('disabled');
26 | json.map(t => t.origin)
27 | .forEach(origin => {
28 | const option = document.createElement('option');
29 | option.value = option.innerText = origin;
30 | originSelect.appendChild(option);
31 | })
32 | });
33 | });
34 | }
35 |
36 | init();
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/META-INF/resources/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 1rem auto;
3 | width: 40rem;
4 | font-family: 'IBM Plex Sans', Arial, sans-serif;
5 | }
6 |
7 | a {
8 | color: #000;
9 | }
10 |
11 | th {
12 | text-align: left
13 | }
14 |
15 | td, th {
16 | padding-right: 0.8rem;
17 | padding-bottom: 0.4rem
18 | }
19 |
20 | .grid {
21 | display: grid;
22 | grid-auto-flow: column;
23 | grid-column-gap: 1rem;
24 | grid-auto-columns: max-content;
25 | }
26 |
27 | .error {
28 | color: red;
29 | font-size: 0.9rem;
30 | }
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | #quarkus.datasource.devservices.init-script-path=scripts/schema-load-data.sql
2 | #quarkus.datasource.devservices.init-script-path=scripts/load-data.sql
3 |
4 | barista.url=http://barista:8080/processes
5 | %dev.barista.url=http://localhost:8002/processes
6 | #%test.barista.url=http://localhost:8002/processes
7 |
8 | #quarkus.hibernate-orm.database.generation=none
9 | #%dev.quarkus.hibernate-orm.database.generation=drop-and-create
10 | #%dev.quarkus.hibernate-orm.sql-load-script=scripts/load-data.sql
11 |
12 |
13 | quarkus.datasource.db-kind=postgresql
14 | quarkus.datasource.jdbc.url=jdbc:postgresql://coffee-shop-db:5432/postgres
15 | %dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres
16 | quarkus.datasource.username=postgres
17 | quarkus.datasource.password=postgres
18 |
19 | #quarkus.hibernate-orm.database.generation=none
20 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/scripts/load-data.sql:
--------------------------------------------------------------------------------
1 | --
2 | -- insert data
3 |
4 | INSERT INTO origins VALUES ('Ethiopia');
5 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'ESPRESSO');
6 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'LATTE');
7 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'POUR_OVER');
8 |
9 | INSERT INTO origins VALUES ('Colombia');
10 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'ESPRESSO');
11 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'POUR_OVER');
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/scripts/schema-load-data.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE public.orders (
2 | id uuid NOT NULL,
3 | status character varying(255) NOT NULL,
4 | type character varying(255) NOT NULL,
5 | origin_name character varying(255) NOT NULL
6 | );
7 |
8 | CREATE TABLE public.origin_coffee_types (
9 | origin_name character varying(255) NOT NULL,
10 | coffee_type character varying(255) NOT NULL
11 | );
12 |
13 | CREATE TABLE public.origins (
14 | name character varying(255) NOT NULL
15 | );
16 |
17 | ALTER TABLE ONLY public.orders
18 | ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
19 |
20 | ALTER TABLE ONLY public.origin_coffee_types
21 | ADD CONSTRAINT origin_coffee_types_pkey PRIMARY KEY (origin_name, coffee_type);
22 |
23 | ALTER TABLE ONLY public.origins
24 | ADD CONSTRAINT origins_pkey PRIMARY KEY (name);
25 |
26 | ALTER TABLE ONLY public.origin_coffee_types
27 | ADD CONSTRAINT fksny5vf8j30otg213opmbp0ab9 FOREIGN KEY (origin_name) REFERENCES public.origins(name);
28 |
29 | ALTER TABLE ONLY public.orders
30 | ADD CONSTRAINT fkt474abpx5pxojjm61tb1d64vp FOREIGN KEY (origin_name) REFERENCES public.origins(name);
31 |
32 | --
33 | -- insert data
34 |
35 | INSERT INTO origins VALUES ('Ethiopia');
36 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'ESPRESSO');
37 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'LATTE');
38 | INSERT INTO origin_coffee_types VALUES ('Ethiopia', 'POUR_OVER');
39 |
40 | INSERT INTO origins VALUES ('Colombia');
41 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'ESPRESSO');
42 | INSERT INTO origin_coffee_types VALUES ('Colombia', 'POUR_OVER');
43 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/scripts/schema.sql:
--------------------------------------------------------------------------------
1 | -- Name: orders; Type: TABLE; Schema: public; Owner: postgres
2 | --
3 |
4 | CREATE TABLE public.orders (
5 | id uuid NOT NULL,
6 | status character varying(255) NOT NULL,
7 | type character varying(255) NOT NULL,
8 | origin_name character varying(255) NOT NULL
9 | );
10 |
11 |
12 | ALTER TABLE public.orders OWNER TO postgres;
13 |
14 | --
15 | -- Name: origin_coffee_types; Type: TABLE; Schema: public; Owner: postgres
16 | --
17 |
18 | CREATE TABLE public.origin_coffee_types (
19 | origin_name character varying(255) NOT NULL,
20 | coffee_type character varying(255) NOT NULL
21 | );
22 |
23 |
24 | ALTER TABLE public.origin_coffee_types OWNER TO postgres;
25 |
26 | --
27 | -- Name: origins; Type: TABLE; Schema: public; Owner: postgres
28 | --
29 |
30 | CREATE TABLE public.origins (
31 | name character varying(255) NOT NULL
32 | );
33 |
34 |
35 | ALTER TABLE public.origins OWNER TO postgres;
36 |
37 | --
38 | -- Name: orders orders_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
39 | --
40 |
41 | ALTER TABLE ONLY public.orders
42 | ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
43 |
44 |
45 | --
46 | -- Name: origin_coffee_types origin_coffee_types_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
47 | --
48 |
49 | ALTER TABLE ONLY public.origin_coffee_types
50 | ADD CONSTRAINT origin_coffee_types_pkey PRIMARY KEY (origin_name, coffee_type);
51 |
52 |
53 | --
54 | -- Name: origins origins_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
55 | --
56 |
57 | ALTER TABLE ONLY public.origins
58 | ADD CONSTRAINT origins_pkey PRIMARY KEY (name);
59 |
60 |
61 | --
62 | -- Name: origin_coffee_types fksny5vf8j30otg213opmbp0ab9; Type: FK CONSTRAINT; Schema: public; Owner: postgres
63 | --
64 |
65 | ALTER TABLE ONLY public.origin_coffee_types
66 | ADD CONSTRAINT fksny5vf8j30otg213opmbp0ab9 FOREIGN KEY (origin_name) REFERENCES public.origins(name);
67 |
68 |
69 | --
70 | -- Name: orders fkt474abpx5pxojjm61tb1d64vp; Type: FK CONSTRAINT; Schema: public; Owner: postgres
71 | --
72 |
73 | ALTER TABLE ONLY public.orders
74 | ADD CONSTRAINT fkt474abpx5pxojjm61tb1d64vp FOREIGN KEY (origin_name) REFERENCES public.origins(name);
75 |
76 |
77 | --
78 | -- PostgreSQL database dump complete
79 | --
80 |
81 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Coffee orders
8 |
9 |
10 |
11 |
12 | All coffee orders
13 |
14 | Create coffee order
15 |
16 |
17 |
18 | Status |
19 | Type |
20 | Origin |
21 |
22 | {#for order in orders}
23 |
24 | {string:capitalize(order.status.name)} |
25 | {order.type.description} |
26 | {order.origin.name} |
27 |
28 | {/for}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/coffee-shop/src/main/resources/templates/order.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Order coffee
8 |
9 |
10 |
11 |
12 | Order coffee
13 |
14 | Show all orders
15 |
16 | {#if failed??}Could not create the order. Please select all values properly.
{/if}
17 |
18 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CoffeeOrderSystem.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.it;
2 |
3 | import jakarta.json.JsonArray;
4 | import jakarta.json.JsonObject;
5 | import jakarta.ws.rs.client.Client;
6 | import jakarta.ws.rs.client.ClientBuilder;
7 | import jakarta.ws.rs.client.WebTarget;
8 | import jakarta.ws.rs.core.MediaType;
9 | import jakarta.ws.rs.core.UriBuilder;
10 | import java.net.URI;
11 | import java.util.List;
12 | import java.util.stream.Collectors;
13 |
14 | class CoffeeOrderSystem {
15 |
16 | private final Client client;
17 | private final WebTarget baseTarget;
18 |
19 | CoffeeOrderSystem() {
20 | client = ClientBuilder.newClient();
21 | baseTarget = client.target(buildBaseUri());
22 | }
23 |
24 | private URI buildBaseUri() {
25 | String host = System.getProperty("coffee-shop.test.host", "localhost");
26 | String port = System.getProperty("coffee-shop.test.port", "8001");
27 | return UriBuilder.fromUri("http://{host}:{port}/").build(host, port);
28 | }
29 |
30 | boolean isSystemUp() {
31 | JsonObject healthJson = retrieveHealthStatus();
32 |
33 | String status = healthJson.getString("status");
34 | if (!"UP".equalsIgnoreCase(status))
35 | return false;
36 |
37 | return "UP".equalsIgnoreCase(extractStatus(healthJson, "coffee-shop"));
38 | }
39 |
40 | private JsonObject retrieveHealthStatus() {
41 | return baseTarget.path("health")
42 | .request(MediaType.APPLICATION_JSON_TYPE)
43 | .get(JsonObject.class);
44 | }
45 |
46 | private String extractStatus(JsonObject healthJson, String name) {
47 | return healthJson.getJsonArray("checks")
48 | .getValuesAs(JsonObject.class)
49 | .stream()
50 | .filter(o -> o.getString("name").equalsIgnoreCase(name))
51 | .map(o -> o.getString("status"))
52 | .findAny().orElse(null);
53 | }
54 |
55 | List getTypes() {
56 | return baseTarget.path("types")
57 | .request(MediaType.APPLICATION_JSON_TYPE)
58 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
59 | .map(o -> o.getString("type"))
60 | .collect(Collectors.toList());
61 | }
62 |
63 | List getOrigins(String type) {
64 | URI typeOriginsUri = retrieveTypeOriginsLink(type);
65 |
66 | return retrieveOriginsForType(typeOriginsUri);
67 | }
68 |
69 | private URI retrieveTypeOriginsLink(String type) {
70 | return baseTarget.path("types")
71 | .request(MediaType.APPLICATION_JSON_TYPE)
72 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
73 | .filter(o -> o.getString("type").equalsIgnoreCase(type))
74 | .map(o -> o.getJsonObject("_links").getString("origins"))
75 | .map(URI::create)
76 | .findAny().orElseThrow(() -> new IllegalStateException("Could not get link to origins for " + type));
77 | }
78 |
79 | private List retrieveOriginsForType(URI typeOriginsUri) {
80 | return client.target(typeOriginsUri)
81 | .request(MediaType.APPLICATION_JSON_TYPE)
82 | .get(JsonArray.class).getValuesAs(JsonObject.class).stream()
83 | .map(o -> o.getString("origin"))
84 | .collect(Collectors.toList());
85 | }
86 |
87 | void close() {
88 | client.close();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CreateOrderQuarkusSmokeIT.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.it;
2 |
3 | import io.quarkus.test.junit.QuarkusTest;
4 | import org.junit.jupiter.api.AfterEach;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Disabled;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 |
11 | @QuarkusTest
12 | @Disabled("too slow")
13 | class CreateOrderQuarkusSmokeIT {
14 |
15 | private CoffeeOrderSystem coffeeOrderSystem;
16 |
17 | @BeforeEach
18 | void setUp() {
19 | System.setProperty("coffee-shop.test.port", "8081");
20 | coffeeOrderSystem = new CoffeeOrderSystem();
21 | }
22 |
23 | @Test
24 | void testIsSystemUp() {
25 | assertThat(coffeeOrderSystem.isSystemUp()).isTrue();
26 | }
27 |
28 | @Test
29 | void testGetTypes() {
30 | assertThat(coffeeOrderSystem.getTypes()).containsExactlyInAnyOrder("Espresso", "Pour_over", "Latte");
31 | }
32 |
33 | @Test
34 | void testGetTypeOrigins() {
35 | assertThat(coffeeOrderSystem.getOrigins("Espresso")).containsExactlyInAnyOrder("Colombia", "Ethiopia");
36 | }
37 |
38 | @AfterEach
39 | void tearDown() {
40 | coffeeOrderSystem.close();
41 | }
42 |
43 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/it/CreateOrderSmokeIT.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.it;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 |
9 | class CreateOrderSmokeIT {
10 |
11 | private CoffeeOrderSystem coffeeOrderSystem;
12 |
13 | @BeforeEach
14 | void setUp() {
15 | coffeeOrderSystem = new CoffeeOrderSystem();
16 | }
17 |
18 | @Test
19 | void testIsSystemUp() {
20 | assertThat(coffeeOrderSystem.isSystemUp()).isTrue();
21 | }
22 |
23 | @Test
24 | void testGetTypes() {
25 | assertThat(coffeeOrderSystem.getTypes()).containsExactlyInAnyOrder("Espresso", "Pour_over", "Latte");
26 | }
27 |
28 | @Test
29 | void testGetTypeOrigins() {
30 | assertThat(coffeeOrderSystem.getOrigins("Espresso")).containsExactlyInAnyOrder("Colombia", "Ethiopia");
31 | }
32 |
33 | @AfterEach
34 | void tearDown() {
35 | coffeeOrderSystem.close();
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/ReflectionSupport.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders;
2 |
3 | import java.lang.reflect.Field;
4 |
5 | public final class ReflectionSupport {
6 |
7 | private ReflectionSupport() {
8 | }
9 |
10 | public static void setReflectiveField(Object object, String fieldName, Object value) {
11 | try {
12 | Field f1 = object.getClass().getDeclaredField(fieldName);
13 | f1.setAccessible(true);
14 | f1.set(object, value);
15 | } catch (ReflectiveOperationException e) {
16 | throw new RuntimeException(e);
17 | }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/TestData.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 |
7 | import java.util.EnumSet;
8 | import java.util.List;
9 | import java.util.Set;
10 | import java.util.UUID;
11 | import java.util.stream.Collectors;
12 | import java.util.stream.Stream;
13 |
14 | public final class TestData {
15 |
16 | private TestData() {
17 | }
18 |
19 | public static List unfinishedOrders() {
20 | Origin colombia = new Origin("Colombia");
21 | return List.of(
22 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, colombia),
23 | new Order(UUID.randomUUID(), CoffeeType.LATTE, colombia),
24 | new Order(UUID.randomUUID(), CoffeeType.POUR_OVER, colombia)
25 | );
26 | }
27 |
28 | public static Set validCoffeeTypes() {
29 | return EnumSet.allOf(CoffeeType.class);
30 | }
31 |
32 | public static List validOrigins() {
33 | Set coffeeTypes = validCoffeeTypes();
34 |
35 | return Stream.of("Colombia", "Ethiopia")
36 | .map(Origin::new)
37 | .peek(o -> o.getCoffeeTypes().addAll(coffeeTypes))
38 | .collect(Collectors.toList());
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopMockitoTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.control.OrderRepository;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 | import org.mockito.ArgumentCaptor;
10 | import org.mockito.Captor;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.jupiter.MockitoExtension;
14 |
15 | import java.util.List;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.mockito.Mockito.*;
19 |
20 | @ExtendWith(MockitoExtension.class)
21 | class CoffeeShopMockitoTest {
22 |
23 | @InjectMocks
24 | CoffeeShop testObject;
25 |
26 | @Captor
27 | ArgumentCaptor orderCaptor;
28 |
29 | private final OrderRepository orderRepository;
30 | private final OrderProcessor orderProcessor;
31 |
32 | CoffeeShopMockitoTest(@Mock OrderRepository orderRepository, @Mock OrderProcessor orderProcessor) {
33 | this.orderRepository = orderRepository;
34 | this.orderProcessor = orderProcessor;
35 | }
36 |
37 | @Test
38 | void testProcessUnfinishedOrders() {
39 | List orders = TestData.unfinishedOrders();
40 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders);
41 |
42 | testObject.processUnfinishedOrders();
43 |
44 | verify(orderRepository).listUnfinishedOrders();
45 |
46 | verify(orderProcessor, times(orders.size())).processOrder(orderCaptor.capture());
47 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders);
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopNaiveTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.Barista;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.control.OrderRepository;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
7 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
8 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus;
9 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
10 | import org.junit.jupiter.api.BeforeEach;
11 | import org.junit.jupiter.api.Test;
12 | import org.mockito.ArgumentCaptor;
13 |
14 | import java.util.Arrays;
15 | import java.util.List;
16 | import java.util.UUID;
17 |
18 | import static com.sebastian_daschner.coffee_shop.orders.ReflectionSupport.setReflectiveField;
19 | import static org.assertj.core.api.Assertions.assertThat;
20 | import static org.mockito.Mockito.*;
21 |
22 | class CoffeeShopNaiveTest {
23 |
24 | private CoffeeShop coffeeShop;
25 | private OrderRepository orderRepository;
26 | private Barista barista;
27 | private ArgumentCaptor orderCaptor;
28 |
29 | @BeforeEach
30 | void setUp() {
31 | coffeeShop = new CoffeeShop();
32 | OrderProcessor orderProcessor = new OrderProcessor();
33 |
34 | coffeeShop.orderProcessor = orderProcessor;
35 | orderRepository = mock(OrderRepository.class);
36 | coffeeShop.orderRepository = orderRepository;
37 |
38 | barista = mock(Barista.class);
39 | setReflectiveField(orderProcessor, "orderRepository", orderRepository);
40 | setReflectiveField(orderProcessor, "barista", barista);
41 | orderCaptor = ArgumentCaptor.forClass(Order.class);
42 |
43 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING);
44 | }
45 |
46 | @Test
47 | void testCreateOrder() {
48 | Order order = new Order();
49 | coffeeShop.createOrder(order);
50 | verify(orderRepository).persist(order);
51 | }
52 |
53 | @Test
54 | void testProcessUnfinishedOrders() {
55 | List orders = Arrays.asList(new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Colombia")),
56 | new Order(UUID.randomUUID(), CoffeeType.ESPRESSO, new Origin("Ethiopia")));
57 |
58 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders);
59 | orders.forEach(o -> when(orderRepository.findById(o.getId())).thenReturn(o));
60 |
61 | coffeeShop.processUnfinishedOrders();
62 |
63 | verify(orderRepository).listUnfinishedOrders();
64 |
65 | verify(barista, times(orders.size())).retrieveOrderStatus(any());
66 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders);
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopQuarkusTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessor;
5 | import com.sebastian_daschner.coffee_shop.orders.control.OrderRepository;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
7 | import io.quarkus.test.junit.QuarkusTest;
8 | import io.quarkus.test.junit.mockito.InjectMock;
9 | import io.quarkus.test.junit.mockito.InjectSpy;
10 | import org.junit.jupiter.api.Disabled;
11 | import org.junit.jupiter.api.Test;
12 |
13 | import jakarta.inject.Inject;
14 | import java.util.List;
15 |
16 | import static org.mockito.Mockito.*;
17 |
18 | @QuarkusTest
19 | @Disabled("too slow")
20 | class CoffeeShopQuarkusTest {
21 |
22 | @Inject
23 | CoffeeShop coffeeShop;
24 |
25 | @InjectMock
26 | OrderRepository orderRepository;
27 |
28 | @InjectSpy
29 | OrderProcessor orderProcessor;
30 |
31 | @Test
32 | void testProcessUnfinishedOrders() {
33 | List orders = TestData.unfinishedOrders();
34 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders);
35 | orders.forEach(o -> when(orderRepository.findById(o.getId())).thenReturn(o));
36 |
37 | coffeeShop.processUnfinishedOrders();
38 |
39 | verify(orderProcessor, times(orders.size())).processOrder(any());
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopTestDouble.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderRepository;
5 | import com.sebastian_daschner.coffee_shop.orders.control.OriginRepository;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
7 |
8 | import java.util.List;
9 |
10 | import static org.mockito.Mockito.*;
11 |
12 | public class CoffeeShopTestDouble extends CoffeeShop {
13 |
14 | public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
15 | orderRepository = mock(OrderRepository.class);
16 | originRepository = mock(OriginRepository.class);
17 | orderProcessor = orderProcessorTestDouble;
18 | }
19 |
20 | public void verifyCreateOrder(Order order) {
21 | verify(orderRepository).persist(order);
22 | }
23 |
24 | public void verifyProcessUnfinishedOrders(List orders) {
25 | verify(orderRepository).listUnfinishedOrders();
26 | ((OrderProcessorTestDouble) orderProcessor).verifyProcessOrders(orders);
27 | }
28 |
29 | public void answerForUnfinishedOrders(List orders) {
30 | when(orderRepository.listUnfinishedOrders()).thenReturn(orders);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/boundary/CoffeeShopUseCaseTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.boundary;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.control.OrderProcessorTestDouble;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import java.util.List;
10 |
11 | class CoffeeShopUseCaseTest {
12 |
13 | private CoffeeShopTestDouble coffeeShop;
14 |
15 | @BeforeEach
16 | void setUp() {
17 | OrderProcessorTestDouble orderProcessor = new OrderProcessorTestDouble();
18 | coffeeShop = new CoffeeShopTestDouble(orderProcessor);
19 | }
20 |
21 | @Test
22 | void testCreateOrder() {
23 | Order order = new Order();
24 | coffeeShop.createOrder(order);
25 | coffeeShop.verifyCreateOrder(order);
26 | }
27 |
28 | @Test
29 | void testProcessUnfinishedOrders() {
30 | List orders = TestData.unfinishedOrders();
31 |
32 | coffeeShop.answerForUnfinishedOrders(orders);
33 |
34 | coffeeShop.processUnfinishedOrders();
35 | coffeeShop.verifyProcessUnfinishedOrders(orders);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderProcessorTestDouble.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.entity.Order;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.OrderStatus;
5 | import org.mockito.ArgumentCaptor;
6 |
7 | import java.util.List;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 | import static org.mockito.ArgumentMatchers.any;
11 | import static org.mockito.Mockito.*;
12 |
13 | public class OrderProcessorTestDouble extends OrderProcessor {
14 |
15 | private final ArgumentCaptor orderCaptor;
16 |
17 | public OrderProcessorTestDouble() {
18 | orderRepository = mock(OrderRepository.class);
19 | barista = mock(Barista.class);
20 | orderCaptor = ArgumentCaptor.forClass(Order.class);
21 |
22 | when(barista.retrieveOrderStatus(orderCaptor.capture())).thenReturn(OrderStatus.PREPARING);
23 | }
24 |
25 | public void verifyProcessOrders(List orders) {
26 | verify(barista, times(orders.size())).retrieveOrderStatus(any());
27 | assertThat(orderCaptor.getAllValues()).containsExactlyElementsOf(orders);
28 | }
29 |
30 | @Override
31 | public void processOrder(Order order) {
32 | when(orderRepository.findById(order.getId())).thenReturn(order);
33 | super.processOrder(order);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/OrderValidatorTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop;
4 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
6 | import org.junit.jupiter.api.BeforeEach;
7 | import org.junit.jupiter.params.ParameterizedTest;
8 | import org.junit.jupiter.params.provider.MethodSource;
9 |
10 | import jakarta.json.Json;
11 | import jakarta.json.JsonObject;
12 | import jakarta.validation.ConstraintValidatorContext;
13 | import java.io.StringReader;
14 | import java.util.Collection;
15 | import java.util.EnumSet;
16 | import java.util.List;
17 | import java.util.Set;
18 |
19 | import static org.assertj.core.api.Assertions.assertThat;
20 | import static org.mockito.Mockito.mock;
21 | import static org.mockito.Mockito.when;
22 |
23 | class OrderValidatorTest {
24 |
25 | private OrderValidator testObject;
26 | private ConstraintValidatorContext context;
27 |
28 | @BeforeEach
29 | void setUp() {
30 | testObject = new OrderValidator();
31 | testObject.coffeeShop = mock(CoffeeShop.class);
32 | context = mock(ConstraintValidatorContext.class);
33 |
34 | Set coffeeTypes = EnumSet.allOf(CoffeeType.class);
35 | Origin colombia = new Origin("Colombia");
36 | colombia.getCoffeeTypes().addAll(coffeeTypes);
37 |
38 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes);
39 | when(testObject.coffeeShop.getOrigin("Colombia")).thenReturn(colombia);
40 | }
41 |
42 | @ParameterizedTest
43 | @MethodSource("validData")
44 | void testIsValid(String json) {
45 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject();
46 |
47 | assertThat(testObject.isValid(jsonObject, context)).isTrue();
48 | }
49 |
50 | private static Collection validData() {
51 | return List.of(
52 | "{\"type\":\"ESPRESSO\",\"origin\":\"Colombia\"}",
53 | "{\"type\":\"Espresso\",\"origin\":\"Colombia\"}",
54 | "{\"type\":\"LATTE\",\"origin\":\"Colombia\"}",
55 | "{\"type\":\"Latte\",\"origin\":\"Colombia\"}",
56 | "{\"type\":\"POUR_OVER\",\"origin\":\"Colombia\"}",
57 | "{\"type\":\"Pour_over\",\"origin\":\"Colombia\"}");
58 | }
59 |
60 | @ParameterizedTest
61 | @MethodSource("invalidData")
62 | void testIsInvalid(String json) {
63 | JsonObject jsonObject = Json.createReader(new StringReader(json)).readObject();
64 |
65 | assertThat(testObject.isValid(jsonObject, context)).isFalse();
66 | }
67 |
68 | private static Collection invalidData() {
69 | return List.of(
70 | "{\"type\":\"SIPHON\",\"origin\":\"Colombia\"}",
71 | "{\"type\":null,\"origin\":\"Colombia\"}",
72 | "{\"origin\":\"Colombia\"}",
73 | "{\"type\":\"ESPRESSO\",\"origin\":\"Ethiopia\"}");
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/RunCucumberTest.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import cucumber.api.CucumberOptions;
4 | import cucumber.api.junit.Cucumber;
5 | import org.junit.runner.RunWith;
6 |
7 | @RunWith(Cucumber.class)
8 | @CucumberOptions
9 | public class RunCucumberTest {
10 |
11 |
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/control/StepDefs.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.control;
2 |
3 | import com.sebastian_daschner.coffee_shop.orders.TestData;
4 | import com.sebastian_daschner.coffee_shop.orders.boundary.CoffeeShop;
5 | import com.sebastian_daschner.coffee_shop.orders.entity.CoffeeType;
6 | import com.sebastian_daschner.coffee_shop.orders.entity.Origin;
7 | import cucumber.api.java.Before;
8 | import cucumber.api.java.en.Then;
9 | import cucumber.api.java.en.When;
10 |
11 | import jakarta.json.Json;
12 | import jakarta.json.JsonObject;
13 | import jakarta.validation.ConstraintValidatorContext;
14 | import java.util.List;
15 | import java.util.Set;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.mockito.ArgumentMatchers.anyString;
19 | import static org.mockito.Mockito.mock;
20 | import static org.mockito.Mockito.when;
21 |
22 | public class StepDefs {
23 |
24 | private OrderValidator testObject;
25 | private ConstraintValidatorContext context;
26 | private JsonObject jsonObject;
27 |
28 | @Before
29 | public void setUp() {
30 | testObject = new OrderValidator();
31 | testObject.coffeeShop = mock(CoffeeShop.class);
32 | context = mock(ConstraintValidatorContext.class);
33 |
34 | List origins = TestData.validOrigins();
35 | Set coffeeTypes = TestData.validCoffeeTypes();
36 |
37 | when(testObject.coffeeShop.getCoffeeTypes()).thenReturn(coffeeTypes);
38 | when(testObject.coffeeShop.getOrigin(anyString())).then(invocation -> origins.stream()
39 | .filter(o -> o.getName().equals(invocation.getArgument(0)))
40 | .findAny()
41 | .orElse(null));
42 | }
43 |
44 | @When("^I create an order with ([^ ]*) from ([^ ]*)$")
45 | public void i_create_an_order(String type, String origin) {
46 | jsonObject = Json.createObjectBuilder()
47 | .add("type", type)
48 | .add("origin", origin)
49 | .build();
50 | }
51 |
52 | @Then("^The order should be accepted$")
53 | public void accepted_order() {
54 | assertThat(testObject.isValid(jsonObject, context)).isTrue();
55 | }
56 |
57 | @Then("^The order should be rejected$")
58 | public void rejected_order() {
59 | assertThat(testObject.isValid(jsonObject, context)).isFalse();
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/java/com/sebastian_daschner/coffee_shop/orders/entity/OrderAssert.java:
--------------------------------------------------------------------------------
1 | package com.sebastian_daschner.coffee_shop.orders.entity;
2 |
3 | import org.assertj.core.api.AbstractAssert;
4 |
5 | public class OrderAssert extends AbstractAssert {
6 |
7 | public OrderAssert(Order order) {
8 | super(order, OrderAssert.class);
9 | }
10 |
11 | public static OrderAssert assertThat(Order actual) {
12 | return new OrderAssert(actual);
13 | }
14 |
15 | public OrderAssert isPreparing() {
16 | isNotNull();
17 | if (actual.getStatus() != OrderStatus.PREPARING) {
18 | failWithMessage("Expected the order to be in status PREPARING but was %s", actual.getStatus());
19 | }
20 | return this;
21 | }
22 |
23 | public OrderAssert containsMilk() {
24 | isNotNull();
25 | if (actual.getType() != CoffeeType.LATTE) {
26 | failWithMessage("Expected the coffee order to contain milk but the coffee type was %s", actual.getType());
27 | }
28 | return this;
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/coffee-shop/src/test/resources/com/sebastian_daschner/coffee_shop/orders/control/validating-coffee-orders.feature:
--------------------------------------------------------------------------------
1 | Feature: Validating coffee order
2 |
3 | Scenario Outline: Creating from , will be
4 | When I create an order with from
5 | Then The order should be
6 |
7 | Examples:
8 | | type | origin | result |
9 | | Espresso | Colombia | accepted |
10 | | Pour_over | Colombia | accepted |
11 | | Espresso | Ethiopia | accepted |
12 | | Latte | Ethiopia | accepted |
13 | | Pour_over | Ethiopia | accepted |
14 | | Espresso | Germany | rejected |
15 | | Siphon | Colombia | rejected |
16 | | Siphon | Germany | rejected |
17 |
--------------------------------------------------------------------------------
/local-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | cd ${0%/*}/coffee-shop
4 |
5 | mvn clean package
6 |
7 | docker build -t coffee-shop .
8 |
--------------------------------------------------------------------------------
/local-run-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | cd ${0%/*}/coffee-shop
4 |
5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true
6 |
7 | docker run -d --rm \
8 | --name coffee-shop-db \
9 | --network dkrnet \
10 | -p 5432:5432 \
11 | -e POSTGRES_USER=postgres \
12 | -e POSTGRES_PASSWORD=postgres \
13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \
14 | postgres:15.2
15 |
16 | docker run -d --rm \
17 | --name barista \
18 | --network dkrnet \
19 | -p 8002:8080 \
20 | sdaschner/barista:quarkus-testing-1
21 |
22 | echo 'waiting for db startup'
23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do
24 | sleep 0.5
25 | done;
26 |
27 | echo 'starting coffee-shop'
28 | docker run -d --rm \
29 | --name coffee-shop \
30 | --network dkrnet \
31 | -p 8001:8080 \
32 | coffee-shop
33 |
34 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do
35 | sleep 0.5
36 | done;
37 | echo 'all containers started'
38 |
39 | #echo 'loading data into database'
40 | #docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql
--------------------------------------------------------------------------------
/presentation/slide001:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ___ __ __ _ _ _ _ _
5 | | __|/ _|/ _|___ __| |_(_)_ _____ | |_ ___ __| |_(_)_ _ __ _
6 | | _|| _| _/ -_) _| _| \ V / -_) | _/ -_|_-< _| | ' \/ _` |
7 | |___|_| |_| \___\__|\__|_|\_/\___| \__\___/__/\__|_|_||_\__, |
8 | |___/
9 | _ _ _ _
10 | | |_| |_ __ _| |_ (_)___
11 | | _| ' \/ _` | _| | (_-<
12 | \__|_||_\__,_|\__| |_/__/
13 |
14 | _ _ _ __
15 | __ _ __| |_ _ _ __ _| | |_ _ / _|_ _ _ _
16 | / _` / _| _| || / _` | | | || | | _| || | ' \
17 | \__,_\__|\__|\_,_\__,_|_|_|\_, | |_| \_,_|_||_|
18 | |__/
19 |
20 |
21 | Sebastian Daschner
22 |
23 |
24 |
--------------------------------------------------------------------------------
/presentation/slide002:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sebastian Daschner
7 |
8 | • Conference speaker, trainer, book author
9 | • Jakarta EE Committer, JCP Expert Group member
10 | • Java Champion
11 | • Oracle Developer Champion (Alumni)
12 | • JavaOne Rockstar speaker
13 |
--------------------------------------------------------------------------------
/presentation/slide003:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | user
6 | \
7 | ┘
8 | .──. ┌───────────┐
9 | │DB│<-->│coffee-shop│
10 | └──┘ └───────────┘
11 |
--------------------------------------------------------------------------------
/presentation/slide004:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | user
6 | \
7 | ┘
8 | .──. ┌───────────┐
9 | │DB│<-->│coffee-shop│
10 | └──┘ └───────────┘
11 | \ async
12 | \
13 | ┘
14 | ┌─────────┐
15 | │ barista │
16 | └─────────┘
17 |
--------------------------------------------------------------------------------
/presentation/slide005:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | _ _ _ _ _
10 | /_\ | |__ __| |_ _ _ __ _ __| |_(_)___ _ _
11 | / _ \| '_ (_-< _| '_/ _` / _| _| / _ \ ' \
12 | /_/ \_\_.__/__/\__|_| \__,_\__|\__|_\___/_||_|
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/presentation/slide006:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | system test
6 | |
7 | v
8 | .──. ┌───────────┐
9 | │DB│<-->│coffee-shop│
10 | └──┘ └───────────┘
11 | |
12 | |
13 | v
14 | ┌─────────┐
15 | │ barista │
16 | │ (mock) │
17 | └─────────┘
18 |
--------------------------------------------------------------------------------
/presentation/slide007:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | system test ---.
6 | | |
7 | v |
8 | .──. ┌───────────┐ |
9 | │DB│<-->│coffee-shop│ | control/verify
10 | └──┘ └───────────┘ |
11 | | |
12 | | |
13 | v |
14 | ┌─────────┐ |
15 | │ barista │ |
16 | │ (mock) │<---’
17 | └─────────┘
18 |
19 |
20 |
--------------------------------------------------------------------------------
/presentation/slide008:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | src/ system test --.
6 | \ | |
7 | \ | |
8 | ┘ v |
9 | .──. ┌───────────┐ |
10 | │DB│<-->│coffee-shop│ | control/verify
11 | └──┘ │ (:dev) │ |
12 | └───────────┘ |
13 | | |
14 | | |
15 | v |
16 | ┌─────────┐ |
17 | │ barista │ |
18 | │ (mock) │<---’
19 | └─────────┘
20 |
21 |
22 |
--------------------------------------------------------------------------------
/presentation/slide009:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Use case tests
5 |
6 |
7 | ┌──────────┐ ┌──────────────┐
8 | │CoffeeShop│ ----> │OrderProcessor│
9 | └──────────┘ └──────────────┘
10 | ^ ^
11 | main | |
12 | ----------------------|--------------------|-------
13 | test | |
14 | ┌────────────┐ ┌───────────┐
15 | │CoffeeShopTD│ ----> │OrderProcTD│
16 | └────────────┘ └───────────┘
17 | \ ┌────┐
18 | ----> │Mock│
19 | └────┘
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/presentation/slide010:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Local development:
5 |
6 | ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌────────────────┐
7 | │ │ │ │ │ │ │ │
8 | │ build/UT │-->│ deploy local │-->│ smoke ITs │-->│ system tests │
9 | │(coffee-shop)│ │ (:dev mode) │ │(coffee-shop)│ │(coffee-shop-st)│
10 | └─────────────┘ └──────────────┘ └─────────────┘ └────────────────┘
11 |
12 |
13 | CI/CD:
14 |
15 | ┌─────────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────────┐
16 | │ │ │ │ │ │ │ │
17 | │ build/UT │-->│ deploy │-->│ smoke ITs │-->│ system tests │-->...
18 | │(coffee-shop)│ │ (e.g. k8s) │ │(point to k8s)│ │ (point to k8s) │
19 | └─────────────┘ └────────────┘ └──────────────┘ └────────────────┘
20 |
--------------------------------------------------------------------------------
/presentation/slide011:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Key Takeaways
7 |
8 | • Test code quality matters
9 | • Reusable test components
10 | • Fast feedback, turnaround cycles
11 | • Separate life cycles
12 | • Reusable, idempotent tests scenarios
13 | • Test code quality > test frameworks
14 |
15 |
--------------------------------------------------------------------------------
/presentation/slide012:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Thank you for your attention!
7 |
8 | • daschner.dev/effective-tests
9 | • @DaschnerS
10 | • Book: Architecting Modern Java EE Applications
11 |
12 | • quarkus.io
13 | • wiremock.org
14 |
--------------------------------------------------------------------------------
/prod-run-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | cd ${0%/*}/coffee-shop
4 |
5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true
6 |
7 | docker run -d --rm \
8 | --name coffee-shop-db \
9 | --network dkrnet \
10 | -p 5432:5432 \
11 | -e POSTGRES_USER=postgres \
12 | -e POSTGRES_PASSWORD=postgres \
13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \
14 | postgres:15.2
15 |
16 | docker run -d --rm \
17 | --name barista \
18 | --network dkrnet \
19 | -p 8002:8080 \
20 | sdaschner/barista:quarkus-testing-1
21 |
22 | echo 'waiting for db startup'
23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do
24 | sleep 0.5
25 | done;
26 |
27 | echo 'creating schema'
28 | docker exec coffee-shop-db psql -U postgres -f /scripts/schema.sql
29 | echo 'loading data into database'
30 | docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql
31 |
32 | echo 'starting coffee-shop'
33 | docker run -d --rm \
34 | --name coffee-shop \
35 | --network dkrnet \
36 | -p 8001:8080 \
37 | coffee-shop
38 |
39 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do
40 | sleep 0.5
41 | done;
--------------------------------------------------------------------------------
/systemtest-run-dev-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 |
4 | cd ${0%/*}/coffee-shop
5 |
6 | trap cleanup EXIT
7 |
8 | function cleanup() {
9 | echo stopping containers
10 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true
11 | }
12 |
13 |
14 | cleanup
15 |
16 | docker run -d --rm \
17 | --name coffee-shop-db \
18 | --network dkrnet \
19 | -p 5432:5432 \
20 | -e POSTGRES_USER=postgres \
21 | -e POSTGRES_PASSWORD=postgres \
22 | postgres:9.5
23 |
24 | docker run -d --rm \
25 | --name barista \
26 | --network dkrnet \
27 | -p 8002:8080 \
28 | rodolpheche/wiremock:2.6.0
29 |
30 |
31 | # coffee-shop
32 | mvn clean package -Dquarkus.package.type=mutable-jar
33 | docker build -t coffee-shop:tmp .
34 |
35 | docker run -d \
36 | --name coffee-shop \
37 | --network dkrnet \
38 | -e QUARKUS_LAUNCH_DEVMODE=true \
39 | -p 8001:8080 \
40 | -p 5005:5005 \
41 | coffee-shop:tmp \
42 | java -jar \
43 | "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" \
44 | -Dquarkus.live-reload.password=123 \
45 | -Dquarkus.http.host=0.0.0.0 \
46 | /deployments/quarkus-run.jar
47 |
48 | # wait for app startup
49 | wget --quiet --tries=30 --waitretry=2 --retry-connrefused -O /dev/null http://localhost:8001/q/health
50 |
51 | mvn quarkus:remote-dev -Ddebug=false -Dquarkus.live-reload.url=http://localhost:8001 -Dquarkus.live-reload.password=123 -Dquarkus.package.type=mutable-jar
52 |
--------------------------------------------------------------------------------
/systemtest-run-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail
3 | cd ${0%/*}/coffee-shop
4 |
5 | docker stop coffee-shop coffee-shop-db barista &> /dev/null || true
6 |
7 | docker run -d --rm \
8 | --name coffee-shop-db \
9 | --network dkrnet \
10 | -p 5432:5432 \
11 | -e POSTGRES_USER=postgres \
12 | -e POSTGRES_PASSWORD=postgres \
13 | -v $(pwd)/src/main/resources/scripts:/scripts:ro \
14 | postgres:15.2
15 |
16 | docker run -d --rm \
17 | --name barista \
18 | --network dkrnet \
19 | -p 8002:8080 \
20 | rodolpheche/wiremock:2.6.0
21 |
22 | echo 'waiting for db startup'
23 | until docker exec coffee-shop-db pg_isready -h localhost > /dev/null; do
24 | sleep 0.5
25 | done;
26 |
27 | echo 'creating schema'
28 | docker exec coffee-shop-db psql -U postgres -f /scripts/schema.sql
29 | echo 'loading data into database'
30 | docker exec coffee-shop-db psql -U postgres -f /scripts/load-data.sql
31 |
32 | echo 'starting coffee-shop'
33 | docker run -d --rm \
34 | --name coffee-shop \
35 | --network dkrnet \
36 | -p 8001:8080 \
37 | coffee-shop
38 |
39 | until [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8001/q/health)" == "200" ]]; do
40 | sleep 0.5
41 | done;
42 |
--------------------------------------------------------------------------------