Solution: Logger

class PetStore( private val database: Database, ) { context(Logger) fun addPet( addPetRequest: AddPetRequest, ): Pet? { logInfo("Adding pet with name ${addPetRequest.name}") return try { database.addPet(addPetRequest) .also { logInfo("Added pet with id ${it.id}") } } catch (e: InsertionConflictException) { logWarning("There already is " + "pet named ${addPetRequest.name}") null } catch (e: Exception) { logError("Failed to add " + "pet with name ${addPetRequest.name}") null } } }

I needed to break the function definition to fit the page. In actual code, I would not do that.

Example solution in playground

import org.junit.After import org.junit.Test import kotlin.random.Random import kotlin.test.assertEquals class PetStore( private val database: Database, ) { context(Logger) fun addPet( addPetRequest: AddPetRequest, ): Pet? { logInfo("Adding pet with name ${addPetRequest.name}") return try { database.addPet(addPetRequest) .also { logInfo("Added pet with id ${it.id}") } } catch (e: InsertionConflictException) { logWarning("There already is " + "pet named ${addPetRequest.name}") null } catch (e: Exception) { logError("Failed to add " + "pet with name ${addPetRequest.name}") null } } } data class AddPetRequest(val name: String) data class Pet(val id: Int, val name: String) class InsertionConflictException : Exception() interface Database { fun addPet(addPetRequest: AddPetRequest): Pet } interface Logger { fun logInfo(message: String) fun logWarning(message: String) fun logError(message: String) } fun main(): Unit = with(ConsoleLogger()) { val database = RandomDatabase() val petStore = PetStore(database) petStore.addPet(AddPetRequest("Fluffy")) // [INFO] - Adding pet with name Fluffy // [INFO] - Added pet with id -81731626 // or // [WARNING] - There already is pet named Fluffy // or // [ERROR] - Failed to add pet with name Fluffy } class RandomDatabase : Database { override fun addPet(addPetRequest: AddPetRequest): Pet = when { Random.nextBoolean() -> Pet(1234, addPetRequest.name) Random.nextBoolean() -> throw InsertionConflictException() else -> throw Exception() } } class ConsoleLogger : Logger { override fun logInfo(message: String) { println("[INFO] - $message") } override fun logWarning(message: String) { println("[WARNING] - $message") } override fun logError(message: String) { println("[ERROR] - $message") } } class PetStoreTest { private val database = FakeDatabase() private val petStore = PetStore(database) private val logger = FakeLogger() @After fun tearDown() { database.clear() logger.clear() } @Test fun `should add pet`() { val pet = with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } val expected = Pet(0, "Fluffy") assertEquals(expected, pet) assertEquals(expected, database.getPets().single()) } @Test fun `should return null when database failing`() { database.startFailing() val pet = with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } assertEquals(null, pet) assertEquals(emptyList<Pet>(), database.getPets()) } @Test fun `should return null when conflict`() { database.addPet(AddPetRequest("Fluffy")) val pet = with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } assertEquals(null, pet) } @Test fun `should log info when added pet`() { with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } assertEquals( listOf( FakeLogger.Level.INFO to "Adding pet with name Fluffy", FakeLogger.Level.INFO to "Added pet with id 0", ), logger.getMessages() ) } @Test fun `should log warning when adding conflict`() { database.addPet(AddPetRequest("Fluffy")) with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } assertEquals( listOf( FakeLogger.Level.INFO to "Adding pet with name Fluffy", FakeLogger.Level.WARNING to "There already is pet named Fluffy", ), logger.getMessages() ) } @Test fun `should log error when database error`() { database.startFailing() with(logger) { petStore.addPet(AddPetRequest("Fluffy")) } assertEquals( listOf( FakeLogger.Level.INFO to "Adding pet with name Fluffy", FakeLogger.Level.ERROR to "Failed to add pet with name Fluffy", ), logger.getMessages() ) } } class FakeLogger : Logger { private val messages = mutableListOf<Pair<Level, String>>() fun clear() { messages.clear() } fun getMessages(): List<Pair<Level, String>> = messages.toList() override fun logInfo(message: String) { messages.add(Level.INFO to message) } override fun logWarning(message: String) { messages.add(Level.WARNING to message) } override fun logError(message: String) { messages.add(Level.ERROR to message) } enum class Level { INFO, WARNING, ERROR } } class FakeDatabase : Database { private val pets = mutableListOf<Pet>() private var shouldFail = false fun startFailing() { shouldFail = true } fun clear() { pets.clear() shouldFail = false } override fun addPet(addPetRequest: AddPetRequest): Pet { if (pets.any { it.name == addPetRequest.name }) throw InsertionConflictException() if (shouldFail) throw Exception() val pet = Pet(pets.size, addPetRequest.name) pets.add(pet) return pet } fun getPets(): List<Pet> = pets.toList() }