Slide 1

Slide 1 text

Exploring the Testing Hyperpyramid with Kotlin & http4k David Denton | Ivan Sanchez KotlinConf 2023

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

What we’re told about the Testing Pyramid Hard, slow, flakey

Slide 4

Slide 4 text

We think it’s more like *this! *Probably not what a real hyperpyramid looks like

Slide 5

Slide 5 text

What makes a good test?

Slide 6

Slide 6 text

Need to master: - Time - Randomness - Concurrency - Networking - Known state/data Barriers for good testing

Slide 7

Slide 7 text

Our example system: eCommerce Website ACTOR SERVICE CLOUD EXTERNAL

Slide 8

Slide 8 text

Technique: Layering with hexagons

Slide 9

Slide 9 text

Technique: Server as a function (Req) -> Rsp (Req) -> Rsp

Slide 10

Slide 10 text

How http4k implements Server as a Function typealias HttpHandler = (Request) -> Response

Slide 11

Slide 11 text

Technique: Events aka Observability Port interface Event typealias Events = (Event) -> Unit data class HttpCall(val uri: Uri) : Event

Slide 12

Slide 12 text

Technique: Screenplay actors interface Customer { fun listItems(): List fun order(id: ItemId, email: String): OrderId }

Slide 13

Slide 13 text

class HttpCustomer(baseUri: Uri, clock: Clock, events: Events, http: HttpHandler) : Customer { val http = ResetRequestTracing() .then(SetHostFrom(baseUri)) .then(AppOutgoingHttp(AppEvents("Customer", clock, events), http)) override fun listItems(): List { val response = http(Request(GET, "/list")) return Body.auto> ().toLens()(response) } override fun order(id: ItemId, email: String): OrderId { val response = http(Request(POST, "/order/$id").query("email", email)) return Body.auto().toLens()(response) } } Implementing the Customer

Slide 14

Slide 14 text

Technique: Fake Adapters fun FakeWarehouse(): HttpHandler { val stockLevels = mutableMapOf(ItemId.of("1") to 5) return routes( "/v1/items" bind GET to { Response(OK) .with( Body.auto > ().toLens() of stockLevels.map { (item, stock) - > InventoryItem(item, "bar", stock) } ) }, "/v1/dispatch" bind POST to { val order = Body.auto().toLens()(it) stockLevels[order.id] = stockLevels[order.id] !! - order.amount Response(ACCEPTED) } ) } fun main() { FakeWarehouse().asServer(SunHttp(8000)).start() }

Slide 15

Slide 15 text

Constructing our service fun ShopApi(env: Environment, clock: Clock, events: Events, http: HttpHandler): HttpHandler { val appEvents = AppEvents("shop", clock, events) val outgoingHttp = AppOutgoingHttp(appEvents, http) val shop = Shop(appEvents, Warehouse.Http(env[WAREHOUSE_URL], outgoingHttp), Notifications.SES(env, outgoingHttp) ) return AppIncomingHttp(appEvents, routes(PlaceOrder(shop), ListAllItems(shop))) }

Slide 16

Slide 16 text

Service testing abstract class RecordTraces { @RegisterExtension val events = TracerBulletEvents( listOf( :: HttpTracer, :: DbTracer).map { it(ActorByService) }, listOf(PumlSequenceDiagram, PumlInteractionDiagram), TraceRenderPersistence.FileSystem(File(".generated")) ) } interface ListItemsScenario { val customer: Customer @Test fun `can list items`() { expectThat(customer.listItems()).isEqualTo(listOf(ItemId.of(“1"))) } } class ShopApiTests : RecordTraces(), ListItemsScenario { val http: HttpHandler = ShopApi(ShopTestEnv, TestClock, events, FakeWarehouse()) override val customer = HttpCustomer(Uri.of("http: // shop"), TestClock, events, http) }

Slide 17

Slide 17 text

Tests as diagrams as code

Slide 18

Slide 18 text

Service composition

Slide 19

Slide 19 text

Server System as a function class EcommerceSystem(env: Environment, clock: Clock, events: Events, theInternet: HttpHandler) : HttpHandler { private val networkAccess = NetworkAccess() private val apiGateway = ApiGateway(env, clock, events, networkAccess) private val shop = ShopApi(env, clock, events, networkAccess) private val warehouse = WarehouseApi(env, clock, events, networkAccess, Inventory.InMemory(events, clock)) init { networkAccess.http = routes( reverseProxyRouting( env[API_GATEWAY_URL].authority to apiGateway, env[SHOP_URL].authority to shop, env[WAREHOUSE_URL].authority to warehouse ), Router.orElse bind theInternet, ) } override fun invoke(request: Request) = networkAccess(request) } http://api-gateway/order/ ECOMMERCE SYSTEM

Slide 20

Slide 20 text

Server Internet as a function http://s3/item-image-1 class TheInternet: HttpHandler { val emails = Storage.InMemory > () val departmentStore = FakeDepartmentStore() val ses = FakeSES(emails) val s3 = FakeS3() val http = reverseProxy( "dept-store" to departmentStore, "email" to ses, "s3" to s3 ) override fun invoke(request: Request) = http(request) } THE INTERNET RP

Slide 21

Slide 21 text

Server System Internet Universe as a function R THE INTERNET UNIVERSE ECOMMERCE SYSTEM

Slide 22

Slide 22 text

Universe testing interface CustomerBuysItemScenario { val storeManager: StoreManager val customer: WebsiteCustomer @Test fun `views item, orders, receives confirmation`() { val itemId = customer.listItems().first() expectThat(customer.canSeeImage(itemId)).isTrue() val orderId = customer.order(itemId, "[email protected]") expectThat(customer.hasEmailFor(orderId, "[email protected]")).isTrue() expectThat(storeManager.hasOrderItems(orderId)).isEqualTo(listOf(itemId)) } } class UniverseTests : RecordTraces(), CustomerBuysItemScenario { val iNet = TheInternet() val env = UniverseTestEnv overrides iNet.createCloudResourcesAndEnv() val clock = TestClock val system = EcommerceSystem(env, clock, events, iNet) override val customer = HttpWebsiteCustomer(env[API_GATEWAY_URL], clock, events, system, iNet.emails) override val storeManager = InternetStoreManager(iNet) }

Slide 23

Slide 23 text

Universe level diagrams

Slide 24

Slide 24 text

After the Big Bang!

Slide 25

Slide 25 text

Practices that make this easier - Monorepo - TDD (outside in) - Contract Testing (3rd party) - ADRs

Slide 26

Slide 26 text

What do we gain? - Shift-left of testing & observability - Massive reuse of testing infra - Higher level tests with the properties of UTs - Test one or more services in the same style - Free documentation!

Slide 27

Slide 27 text

http4k v5* and beyond *coming April ’23. Contains Loom. +

Slide 28

Slide 28 text

Thank you!
 Don’t forget
 to vote. David & Ivan Talk materials @ https://http4k.org/hyperpyramid