Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Exploring the Testing Hyperpyramid with Kotlin & http4k

Exploring the Testing Hyperpyramid with Kotlin & http4k

We all know that testing is an important factor in software development.


What if your tests could also work for you in dimensions that you didn’t even know existed? What if they could give you superpowers? Superpowers like easily running an entire bank on a single developer workstation, generating visual documentation of all the interactions across your fleet of microservices with a simple test plugin, or building reusable infrastructure which allow you to test your entire codebase at the unit, integration or end-to-end level.

In this talk, you'll learn about why your design and technology choices matter for testing and some Kotlin and http4k techniques that allow you to do much more by actually writing less test code.

David Denton

April 13, 2023

More Decks by David Denton

Other Decks in Programming


  1. Need to master: - Time - Randomness - Concurrency -

    Networking - Known state/data Barriers for good testing
  2. Technique: Events aka Observability Port interface Event typealias Events =

    (Event) -> Unit data class HttpCall(val uri: Uri) : Event
  3. 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<ItemId> { val response = http(Request(GET, "/list")) return Body.auto<List<ItemId >> ().toLens()(response) } override fun order(id: ItemId, email: String): OrderId { val response = http(Request(POST, "/order/$id").query("email", email)) return Body.auto<OrderId>().toLens()(response) } } Implementing the Customer
  4. 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<List<InventoryItem > > ().toLens() of stockLevels.map { (item, stock) - > InventoryItem(item, "bar", stock) } ) }, "/v1/dispatch" bind POST to { val order = Body.auto<Shipment>().toLens()(it) stockLevels[order.id] = stockLevels[order.id] !! - order.amount Response(ACCEPTED) } ) } fun main() { FakeWarehouse().asServer(SunHttp(8000)).start() }
  5. 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))) }
  6. 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) }
  7. 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
  8. Server Internet as a function http://s3/item-image-1 class TheInternet: HttpHandler {

    val emails = Storage.InMemory<List<EmailMessage > > () 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
  9. 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) }
  10. Practices that make this easier - Monorepo - TDD (outside

    in) - Contract Testing (3rd party) - ADRs
  11. 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!
  12. Thank you!
 Don’t forget
 to vote. David & Ivan Talk

    materials @ https://http4k.org/hyperpyramid