commit 4df4f6351f58fb954c71fe32a9aef71ab6258949 Author: Sander Hautvast Date: Tue Mar 8 09:31:05 2022 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83cdb7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.idea/ +target/ +*.iml +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3215b6b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Nikola Kasev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f311a9 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# workshop-baker +A workshop for Java developers to get started with Baker and develop an intuition for implementing complex orchestration logic in APIs. Start [here](docs/baker-workshop.pdf). diff --git a/docs/baker-workshop.md b/docs/baker-workshop.md new file mode 100644 index 0000000..00f4981 --- /dev/null +++ b/docs/baker-workshop.md @@ -0,0 +1,207 @@ +![](workshop.jpg) + +--- + +# Workshop Agenda +## Why Baker? (10 minutes) +## Develop an Intuition for Baker (15 m.) +## Implement a Web Shop Flow (25 m.) + +--- + +# Why Baker? + +--- + +## Our Challenge +### Interact with 12 Different Systems (complex environment) +### A Flow of 27 Steps (difficult to reason about the logic) +### From 2 minutes to 6 hours (stateful...) + +--- + +## Requirements +### TIBCO is contained +### Java developers +### State in two data-centres +### Systems fail all the time + +--- + +| Current Account | Savings Account | Customer Onboarding | +| :--- | :---: | ---: | +| Verify Identity | Verify Identity | Verify Identity | +| Register Individual | Register Individual | Register Individual | +| Open *Current* Account | Open *Savings* Account | `n/a` | +| Issue Debit Card | `n/a` | `n/a` | +| Send Message | Send Message | Send Message | +| Register Ownership | Register Ownership | `n/a` | + +--- + +![original 270%](baker.png) + +--- +## Baker is a Scala Library +### Declare the Logic Like a Recipe +### Visualize the Logic +### Don't Worry About Retries and State +### Re-use What's Already There + +--- + +![left](donuts.jpg) + +# Recipes +## Events +## Ingredients +## Interactions + +--- +![left, fit](sensory-and-system-events.png) +# What is an Event? +## It's what happens +## Sensory and System Events + +--- +![left, fit](ingredient.png) +# What is an Ingredient? +## It's a container for data +## Can be a primitive type or a POJO + +--- +![left, fit](interaction.png) +# What is an Interaction? +## It's where you put your system calls +## Capability, not a technical call + +--- +# Hands-On +## Setup Environment +## Explain the Web-shop Flow +## Exercise, Make Unit-tests Green + +--- +## Setup Environment +- clone https://github.com/nikolakasev/workshop-baker +- mvn clean test + - tests fail on purpose + - do the [exercises](#exercises) to get them green + +--- +[.background-color: #FFFFFF] +![fit](sequence.png) + +--- +[.background-color: #FFFFFF] +![fit](validate-order.png) + +--- +[.background-color: #FFFFFF] +![fit](manufacture-goods.png) + +--- +[.background-color: #FFFFFF] +![fit](ship-goods.png) + +--- +[.background-color: #FFFFFF] +![fit](send-invoice.png) + +--- + + +## Exercises +1. Put all interactions in the recipe +2. Include sensory events in the recipe, use withSensoryEvents() +3. Add extra conditions when an interaction must be executed, use withRequiredEvent() +4. Change the customerInfo class from String to a POJO + +--- +[.background-color: #FFFFFF] +![left, fit](sequence-parallel.png) + +# 5. Call the Postoffice and Accountant Systems in parallel + +--- + +# What Can Go Wrong? + +--- +[.background-color: #FFFFFF] +![left, fit](no-sensory-input.png) + +# Recipe has no sensory input, Baker doesn't know where the data comes from + +--- +[.background-color: #FFFFFF] +![left, fit](incorrect-sequence.png) + +# Which two interactions will be executed when the OrderPlaced event occurs? Is this the desired behavior? + +--- +# Desired Recipe + +--- +[.background-color: #FFFFFF] +![fit](desired-end-result.png) + +--- +# Food for Thought +- What to do when the order is invalid? + - Baker helps developers think about rainy-day scenarios +- What if systems fails, what is desired? + - See withFailureStrategy(), withDefaultFailureStrategy() methods and InteractionFailureStrategy class + +--- +# Food for Thought / 2 +- How to agree on functionality with stakeholders? + - Baker represents the Java logic visually +- How to change existing functionality? How to be confident nothing breaks? + - Baker validates the recipe and reports inconsistencies (such as missing ingredients) + +--- +# Good to Know + +--- + +## Short-lived vs. long-running flows + +--- + +## State is taken care of: + +- Cassandra for persistent storage +- Ingredients encrypted by default +- State recovered automatically +- Configurable Time-to-live + +--- + +## When failure occurs: + +- Baker retries technical failures with exponential backoff + - Any Java Exception thrown is a technical failure + - Retrying works well with **idempotent** services +- Deal with functional failure in your recipe + +--- + +## Baker Catalogue: Some Stats +### Internal ING library +### 80+ Re-usable Interactions +### 10+ Teams Using Baker + +--- + +## Baker vs. PEGA + +| Baker | PEGA | +| :--- | :---: | +| Computer | Computer + Human | +| Focused on APIs | Focused on processes | +| No License Costs | License Costs | +| No Usage Costs | Pay per Case Costs | +| For Java Developers | For PEGA Specialists | +| From ING | From PEGA | + diff --git a/docs/baker-workshop.pdf b/docs/baker-workshop.pdf new file mode 100644 index 0000000..42c54a8 Binary files /dev/null and b/docs/baker-workshop.pdf differ diff --git a/docs/baker.png b/docs/baker.png new file mode 100644 index 0000000..ee8e44a Binary files /dev/null and b/docs/baker.png differ diff --git a/docs/desired-end-result.png b/docs/desired-end-result.png new file mode 100644 index 0000000..ab40c1f Binary files /dev/null and b/docs/desired-end-result.png differ diff --git a/docs/donuts.jpg b/docs/donuts.jpg new file mode 100644 index 0000000..91059c8 Binary files /dev/null and b/docs/donuts.jpg differ diff --git a/docs/incorrect-sequence.png b/docs/incorrect-sequence.png new file mode 100644 index 0000000..27b0877 Binary files /dev/null and b/docs/incorrect-sequence.png differ diff --git a/docs/ingredient.png b/docs/ingredient.png new file mode 100644 index 0000000..19395d9 Binary files /dev/null and b/docs/ingredient.png differ diff --git a/docs/interaction.png b/docs/interaction.png new file mode 100644 index 0000000..099a334 Binary files /dev/null and b/docs/interaction.png differ diff --git a/docs/manufacture-goods.png b/docs/manufacture-goods.png new file mode 100644 index 0000000..69e3941 Binary files /dev/null and b/docs/manufacture-goods.png differ diff --git a/docs/manufacture-goods.svg b/docs/manufacture-goods.svg new file mode 100644 index 0000000..2e7f181 --- /dev/null +++ b/docs/manufacture-goods.svg @@ -0,0 +1,67 @@ + + + %3 + + + + GoodsManufactured + + + GoodsManufactured + + + + + goods + + + goods + + + + + GoodsManufactured->goods + + + + + + order + + + order + + + + + ManufactureGoods + + + ManufactureGoods + + + + + order->ManufactureGoods + + + + + + ManufactureGoods->GoodsManufactured + + + + + diff --git a/docs/no-sensory-input.png b/docs/no-sensory-input.png new file mode 100644 index 0000000..e7c8abf Binary files /dev/null and b/docs/no-sensory-input.png differ diff --git a/docs/send-invoice.png b/docs/send-invoice.png new file mode 100644 index 0000000..185093b Binary files /dev/null and b/docs/send-invoice.png differ diff --git a/docs/send-invoice.svg b/docs/send-invoice.svg new file mode 100644 index 0000000..fb814ff --- /dev/null +++ b/docs/send-invoice.svg @@ -0,0 +1,51 @@ + + + %3 + + + + InvoiceSent + + + InvoiceSent + + + + + SendInvoice + + + SendInvoice + + + + + SendInvoice->InvoiceSent + + + + + + customerInfo + + + customerInfo + + + + + customerInfo->SendInvoice + + + + + diff --git a/docs/sensory-and-system-events.png b/docs/sensory-and-system-events.png new file mode 100644 index 0000000..71d1047 Binary files /dev/null and b/docs/sensory-and-system-events.png differ diff --git a/docs/sequence-parallel.png b/docs/sequence-parallel.png new file mode 100644 index 0000000..f53937b Binary files /dev/null and b/docs/sequence-parallel.png differ diff --git a/docs/sequence.png b/docs/sequence.png new file mode 100644 index 0000000..f158cce Binary files /dev/null and b/docs/sequence.png differ diff --git a/docs/sequence.puml b/docs/sequence.puml new file mode 100644 index 0000000..ce564de --- /dev/null +++ b/docs/sequence.puml @@ -0,0 +1,21 @@ +@startuml +actor Customer as C +participant "Web-shop API" as API +participant ERP +participant Factory +participant PostOffice +participant Accountant +C -> API : Lookup product +activate API +API -> ERP : Validate Order (check availability) +API -> C : OK +deactivate API +C -> API : Order product +activate API +API -> C : OK +API -> ERP : Validate Order (check availability) +API -> Factory : Manufacture Product +API -> PostOffice : Ship Product +API -> Accountant : Invoice Customer +deactivate API +@enduml diff --git a/docs/ship-goods.png b/docs/ship-goods.png new file mode 100644 index 0000000..70c057d Binary files /dev/null and b/docs/ship-goods.png differ diff --git a/docs/ship-goods.svg b/docs/ship-goods.svg new file mode 100644 index 0000000..966f3b2 --- /dev/null +++ b/docs/ship-goods.svg @@ -0,0 +1,68 @@ + + + %3 + + + + ShipGoods + + + ShipGoods + + + + + GoodsShipped + + + GoodsShipped + + + + + ShipGoods->GoodsShipped + + + + + + goods + + + goods + + + + + goods->ShipGoods + + + + + + customerInfo + + + customerInfo + + + + + customerInfo->ShipGoods + + + + + diff --git a/docs/validate-order.png b/docs/validate-order.png new file mode 100644 index 0000000..3bfa9a8 Binary files /dev/null and b/docs/validate-order.png differ diff --git a/docs/validate-order.svg b/docs/validate-order.svg new file mode 100644 index 0000000..444829b --- /dev/null +++ b/docs/validate-order.svg @@ -0,0 +1,68 @@ + + + %3 + + + + OrderValid + + + OrderValid + + + + + OrderInvalid + + + OrderInvalid + + + + + order + + + order + + + + + ValidateOrder + + + ValidateOrder + + + + + order->ValidateOrder + + + + + + ValidateOrder->OrderValid + + + + + + ValidateOrder->OrderInvalid + + + + + diff --git a/docs/wkshop_evaluation.pdf b/docs/wkshop_evaluation.pdf new file mode 100644 index 0000000..995fbc6 Binary files /dev/null and b/docs/wkshop_evaluation.pdf differ diff --git a/docs/workshop.jpg b/docs/workshop.jpg new file mode 100644 index 0000000..2c622b5 Binary files /dev/null and b/docs/workshop.jpg differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d21e908 --- /dev/null +++ b/pom.xml @@ -0,0 +1,178 @@ + + + + 4.0.0 + + com.ing.baker.tutorials + workshop + 1.1-SNAPSHOT + + workshop + https://github.com/ing-bank/baker + + + UTF-8 + 1.8 + 1.8 + 1.10 + 1.10.19 + 3.3.3 + 1.6.10 + 5.4.2 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + + guru.nidi + graphviz-java + 0.5.2 + test + + + + org.apache.xmlgraphics + batik-i18n + ${apache.batik.version} + test + + + org.apache.xmlgraphics + batik-constants + ${apache.batik.version} + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito.kotlin + mockito-kotlin + 4.0.0 + test + + + + com.ing.baker + baker-recipe-dsl_2.12 + ${baker.version} + + + com.ing.baker + baker-runtime_2.12 + ${baker.version} + + + com.ing.baker + baker-compiler_2.12 + ${baker.version} + + + + org.projectlombok + lombok + 1.16.16 + + + com.google.guava + guava + 29.0-jre + + + + + + + + maven-clean-plugin + 3.0.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.7.0 + + + maven-surefire-plugin + 2.20.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + + compile + + compile + + + + + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + diff --git a/src/main/kotlin/com/ing/baker/tutorials/WebShopRecipe.kt b/src/main/kotlin/com/ing/baker/tutorials/WebShopRecipe.kt new file mode 100644 index 0000000..f033c0d --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/WebShopRecipe.kt @@ -0,0 +1,26 @@ +package com.ing.baker.tutorials + +import com.ing.baker.recipe.javadsl.InteractionDescriptor +import com.ing.baker.recipe.javadsl.Recipe +import com.ing.baker.tutorials.events.SensoryEvents.* +import com.ing.baker.tutorials.events.ShipGoodsEvents +import com.ing.baker.tutorials.interactions.ManufactureGoods +import com.ing.baker.tutorials.interactions.SendInvoice +import com.ing.baker.tutorials.interactions.ShipGoods +import com.ing.baker.tutorials.interactions.ValidateOrder + +class WebShopRecipe { + companion object { + fun getRecipe(): Recipe { + return Recipe("Webshop") + .withSensoryEvents(OrderPlaced::class.java, PaymentMade::class.java, CustomerInfoReceived::class.java) + .withInteractions( + InteractionDescriptor.of(ValidateOrder::class.java), + InteractionDescriptor.of(ManufactureGoods::class.java).withRequiredEvent(PaymentMade::class.java), + InteractionDescriptor.of(ShipGoods::class.java), + InteractionDescriptor.of(SendInvoice::class.java) + .withRequiredEvent(ShipGoodsEvents.GoodsShipped::class.java) + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/events/ManufactureGoodsEvents.kt b/src/main/kotlin/com/ing/baker/tutorials/events/ManufactureGoodsEvents.kt new file mode 100644 index 0000000..9c14ea1 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/events/ManufactureGoodsEvents.kt @@ -0,0 +1,8 @@ +package com.ing.baker.tutorials.events + +class ManufactureGoodsEvents { + + interface ManufactureOutcome + + class GoodsManufactured(val goods: String): ManufactureOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/events/SendInvoiceEvents.kt b/src/main/kotlin/com/ing/baker/tutorials/events/SendInvoiceEvents.kt new file mode 100644 index 0000000..1a70c12 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/events/SendInvoiceEvents.kt @@ -0,0 +1,8 @@ +package com.ing.baker.tutorials.events + +class SendInvoiceEvents { + + interface InvoicingOutcome + + class InvoiceSent: InvoicingOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/events/SensoryEvents.kt b/src/main/kotlin/com/ing/baker/tutorials/events/SensoryEvents.kt new file mode 100644 index 0000000..f80ae30 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/events/SensoryEvents.kt @@ -0,0 +1,12 @@ +package com.ing.baker.tutorials.events + +import com.ing.baker.tutorials.model.CustomerInfo + +class SensoryEvents { + + class OrderPlaced(val order: String) + + class CustomerInfoReceived(val customerInfo: CustomerInfo) + + class PaymentMade +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/events/ShipGoodsEvents.kt b/src/main/kotlin/com/ing/baker/tutorials/events/ShipGoodsEvents.kt new file mode 100644 index 0000000..c82074e --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/events/ShipGoodsEvents.kt @@ -0,0 +1,7 @@ +package com.ing.baker.tutorials.events + +class ShipGoodsEvents { + interface ShippingOutcome + + class GoodsShipped: ShippingOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/events/ValidateOrderEvents.kt b/src/main/kotlin/com/ing/baker/tutorials/events/ValidateOrderEvents.kt new file mode 100644 index 0000000..d70840f --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/events/ValidateOrderEvents.kt @@ -0,0 +1,9 @@ +package com.ing.baker.tutorials.events + +class ValidateOrderEvents { + interface ValidateOrderOutcome + + class OrderValid: ValidateOrderOutcome + + class OrderInvalid: ValidateOrderOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/interactions/ManufactureGoods.kt b/src/main/kotlin/com/ing/baker/tutorials/interactions/ManufactureGoods.kt new file mode 100644 index 0000000..68e533b --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/interactions/ManufactureGoods.kt @@ -0,0 +1,13 @@ +package com.ing.baker.tutorials.interactions + +import com.ing.baker.recipe.annotations.FiresEvent +import com.ing.baker.recipe.annotations.RequiresIngredient +import com.ing.baker.recipe.javadsl.Interaction +import com.ing.baker.tutorials.events.ManufactureGoodsEvents.GoodsManufactured +import com.ing.baker.tutorials.events.ManufactureGoodsEvents.ManufactureOutcome + +interface ManufactureGoods: Interaction { + @FiresEvent(oneOf = [GoodsManufactured::class]) + @Throws(Exception::class) + fun apply(@RequiresIngredient("order") order: String?): ManufactureOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/interactions/SendInvoice.kt b/src/main/kotlin/com/ing/baker/tutorials/interactions/SendInvoice.kt new file mode 100644 index 0000000..e6fecd5 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/interactions/SendInvoice.kt @@ -0,0 +1,14 @@ +package com.ing.baker.tutorials.interactions + +import com.ing.baker.recipe.annotations.FiresEvent +import com.ing.baker.recipe.annotations.RequiresIngredient +import com.ing.baker.recipe.javadsl.Interaction +import com.ing.baker.tutorials.events.SendInvoiceEvents +import com.ing.baker.tutorials.events.SendInvoiceEvents.InvoicingOutcome +import com.ing.baker.tutorials.model.CustomerInfo + +interface SendInvoice : Interaction { + + @FiresEvent(oneOf = [SendInvoiceEvents.InvoiceSent::class]) + fun apply(@RequiresIngredient("customerInfo") customerInfo: CustomerInfo): InvoicingOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/interactions/ShipGoods.kt b/src/main/kotlin/com/ing/baker/tutorials/interactions/ShipGoods.kt new file mode 100644 index 0000000..3d38d93 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/interactions/ShipGoods.kt @@ -0,0 +1,16 @@ +package com.ing.baker.tutorials.interactions + +import com.ing.baker.recipe.annotations.FiresEvent +import com.ing.baker.recipe.annotations.RequiresIngredient +import com.ing.baker.recipe.javadsl.Interaction +import com.ing.baker.tutorials.events.ShipGoodsEvents.GoodsShipped +import com.ing.baker.tutorials.model.CustomerInfo + +interface ShipGoods : Interaction { + + @FiresEvent(oneOf = [GoodsShipped::class]) + fun apply( + @RequiresIngredient("goods") goods: String, + @RequiresIngredient("customerInfo") customerInfo: CustomerInfo + ): GoodsShipped +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrder.kt b/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrder.kt new file mode 100644 index 0000000..e93ad71 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrder.kt @@ -0,0 +1,12 @@ +package com.ing.baker.tutorials.interactions + +import com.ing.baker.recipe.annotations.FiresEvent +import com.ing.baker.recipe.annotations.RequiresIngredient +import com.ing.baker.recipe.javadsl.Interaction +import com.ing.baker.tutorials.events.ValidateOrderEvents.* + +interface ValidateOrder: Interaction { + + @FiresEvent(oneOf = [OrderValid::class, OrderInvalid::class]) + fun apply(@RequiresIngredient("order") order: String): ValidateOrderOutcome +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrderImpl.kt b/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrderImpl.kt new file mode 100644 index 0000000..005bda0 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/interactions/ValidateOrderImpl.kt @@ -0,0 +1,10 @@ +package com.ing.baker.tutorials.interactions + +import com.ing.baker.tutorials.events.ValidateOrderEvents + +class ValidateOrderImpl: ValidateOrder { + override fun apply(order: String): ValidateOrderEvents.ValidateOrderOutcome { + print("Validation order...") + return ValidateOrderEvents.OrderValid() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ing/baker/tutorials/model/CustomerInfo.kt b/src/main/kotlin/com/ing/baker/tutorials/model/CustomerInfo.kt new file mode 100644 index 0000000..49cecc3 --- /dev/null +++ b/src/main/kotlin/com/ing/baker/tutorials/model/CustomerInfo.kt @@ -0,0 +1,3 @@ +package com.ing.baker.tutorials.model + +data class CustomerInfo(val info: String) \ No newline at end of file diff --git a/src/test/kotlin/com/ing/baker/tutorials/WebShopRecipeTest.kt b/src/test/kotlin/com/ing/baker/tutorials/WebShopRecipeTest.kt new file mode 100644 index 0000000..1b2f73d --- /dev/null +++ b/src/test/kotlin/com/ing/baker/tutorials/WebShopRecipeTest.kt @@ -0,0 +1,129 @@ +package com.ing.baker.tutorials + +import akka.actor.ActorSystem +import com.ing.baker.compiler.RecipeCompiler +import com.ing.baker.runtime.akka.AkkaBaker +import com.ing.baker.runtime.javadsl.EventInstance +import com.ing.baker.runtime.javadsl.InteractionInstance +import com.ing.baker.tutorials.events.ManufactureGoodsEvents.GoodsManufactured +import com.ing.baker.tutorials.events.SendInvoiceEvents.InvoiceSent +import com.ing.baker.tutorials.events.SensoryEvents +import com.ing.baker.tutorials.events.ShipGoodsEvents.GoodsShipped +import com.ing.baker.tutorials.events.ValidateOrderEvents.OrderValid +import com.ing.baker.tutorials.interactions.ManufactureGoods +import com.ing.baker.tutorials.interactions.SendInvoice +import com.ing.baker.tutorials.interactions.ShipGoods +import com.ing.baker.tutorials.interactions.ValidateOrder +import com.ing.baker.tutorials.model.CustomerInfo +import guru.nidi.graphviz.engine.Format +import guru.nidi.graphviz.engine.Graphviz +import guru.nidi.graphviz.parse.Parser +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.* +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import scala.collection.mutable.ArraySeq +import scala.concurrent.Await +import scala.concurrent.duration.Duration +import java.io.File +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class WebShopRecipeTest { + private val recipe = WebShopRecipe.getRecipe() + private val mockValidateOrder = mock() + private val mockManufactureGoods = mock() + private val mockShipGoods = mock() + private val mockSendInvoice = mock() + + private val actorSystem = ActorSystem.create("WebShop") + private val baker = AkkaBaker.javaLocalDefault(actorSystem, listOf(InteractionInstance.from(mockValidateOrder), + InteractionInstance.from(mockManufactureGoods), + InteractionInstance.from(mockShipGoods), + InteractionInstance.from(mockSendInvoice))) + + private var recipeId: String + + private val orderId = "ORD-123456" + private val customerInfo = CustomerInfo("John Doe, Amsterdam") + private val goods = "this is the product" + + init { + val compiledRecipe = RecipeCompiler.compileRecipe(recipe) + recipeId = baker.addRecipe(compiledRecipe, true).get() + } + + @AfterEach + fun stopActorSystem() { + Await.ready(actorSystem.terminate(), Duration.apply(20, TimeUnit.SECONDS)) + } + + @Test + fun shouldVisualiseTheRecipe() { + val compiledRecipe = RecipeCompiler.compileRecipe(recipe) + val visualRecipe = compiledRecipe.recipeVisualization + saveVisualizationAsSvg(visualRecipe) + } + + @Test + fun shouldHaveNoValidationErrors() { + val compiledRecipe = RecipeCompiler.compileRecipe(recipe) + assertEquals(compiledRecipe.validationErrors(), ArraySeq.empty()) + } + + @Test + fun shouldBakeTheRecipe() { + val processId = UUID.randomUUID().toString() + baker.bake(recipeId, processId) + val accumulatedState = baker.getIngredients(processId) + assertNotNull(accumulatedState) + } + + @Test + fun shouldExecuteHappyFowCorrectly() { + setupMocks() + + val processId = UUID.randomUUID().toString() + baker.bake(recipeId, processId) + + baker.fireEventAndResolveWhenCompleted(processId, EventInstance.from(SensoryEvents.CustomerInfoReceived(customerInfo))).get() + verify(mockValidateOrder, never()).apply(anyString()) + verify(mockManufactureGoods, never()).apply(anyString()) + verify(mockShipGoods, never()).apply(goods, customerInfo) + verify(mockSendInvoice, never()).apply(customerInfo) + + baker.fireEventAndResolveWhenCompleted(processId, EventInstance.from(SensoryEvents.OrderPlaced(orderId))).get() + verify(mockValidateOrder).apply(orderId) + verify(mockManufactureGoods, never()).apply(anyString()) + verify(mockShipGoods, never()).apply(goods, customerInfo) + verify(mockSendInvoice, never()).apply(customerInfo) + + baker.fireEventAndResolveWhenCompleted(processId, EventInstance.from(SensoryEvents.PaymentMade())).get() + verify(mockManufactureGoods).apply(orderId) + verify(mockShipGoods).apply(goods, customerInfo) + verify(mockSendInvoice).apply(customerInfo) + + verify(mockValidateOrder, times(1)).apply(orderId) + verify(mockManufactureGoods, times(1)).apply(orderId) + verify(mockShipGoods, times(1)).apply(goods, customerInfo) + verify(mockSendInvoice, times(1)).apply(customerInfo) + } + + private fun saveVisualizationAsSvg(dotGraph: String) { + val file = File("./target/" + "web-shop-recipe.svg") + val g = Parser.read(dotGraph) + Graphviz.fromGraph(g).render(Format.SVG).toFile(file) + } + + private fun setupMocks() { + `when`(mockValidateOrder.apply(orderId)).thenReturn(OrderValid()) + `when`(mockManufactureGoods.apply(orderId)).thenReturn(GoodsManufactured(goods)) + `when`(mockShipGoods.apply(goods, customerInfo)).thenReturn(GoodsShipped()) + `when`(mockSendInvoice.apply(customerInfo)).thenReturn(InvoiceSent()) + } + +} \ No newline at end of file diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf new file mode 100644 index 0000000..d8b37f4 --- /dev/null +++ b/src/test/resources/application.conf @@ -0,0 +1 @@ +include "baker.conf" \ No newline at end of file