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()
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.