Your task is to implement a simple library for interface mocking. You should only support mocking interface methods that do not throw exceptions. This is how you should use it:
data class User(val name: String)
interface UserRepository {
fun getUser(userId: String): User?
fun allUsers(): List<User>
}
interface UserService {
fun getUser(): User
}
Your implementation should use Proxy from the java.lang.reflect package.
This problem can either be solved in the below playground or you can clone kotlin-exercises project and solve it locally. In the project, you can find code template for this exercise in advanced/reflection/Mocking.kt. You can find there starting code, example usage and unit tests.
Hints:
A mocked method can be called to both record and use the mock. To know what situation it is, define a recording flag in your class.
Each call is identified by three things: mock object identifier, method name, and method arguments. You can define a data class to keep those three things.
Once you are done with the exercise, you can check your solution here.
Playground
import org.junit.Test
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import kotlin.reflect.KClass
import java.lang.reflect.InvocationHandler
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
data class User(val name: String)
interface UserRepository {
fun getUser(userId: String): User?
fun allUsers(): List<User>
}
interface UserService {
fun getUser(): User
}
fun main() {
val registry = MockRegistry()
val userRepository = registry.mock<UserRepository>()
val userService = registry.mock<UserService>()
registry.setReturnValue(
{ userRepository.getUser("alex") },
User("Alex Smith")
)
registry.setReturnValue(
{ userRepository.getUser("bell") },
User("Bell Rogers")
)
registry.setReturnValue(
{ userRepository.getUser("dan") },
null
)
registry.setBody({ userRepository.allUsers() }) {
listOf(User("James Bond"), User("Jane Doe"))
}
registry.setBody({ userService.getUser() }) {
User(userRepository.getUser("dan")?.name ?: "Unknown")
}
println(userRepository.getUser("alex"))
// User(name=Alex Smith)
println(userRepository.allUsers())
// [User(name=James Bond), User(name=Jane Doe)]
println(userRepository.getUser("bell"))
// User(name=Bell Rogers)
println(userService.getUser())
// User(name=Unknown)
registry.setReturnValue(
{ userRepository.getUser("dan") },
User("Dan Brown")
)
println(userService.getUser())
// User(name=Dan Brown)
}
class MockingTest {
interface GetValue {
fun getInt(): Int
fun getIntWithArg(arg: Int): Int
fun getString(): String
}
@Test
fun `should throw exception when no value set`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
val result = runCatching {
mock.getInt()
}
val exception = result.exceptionOrNull()
assertNotNull(exception)
assert(exception is IllegalStateException)
assertEquals("No handler for method getInt()", exception.message)
val result2 = runCatching {
mock.getIntWithArg(123)
}
val exception2 = result2.exceptionOrNull()
assertNotNull(exception2)
assert(exception2 is IllegalStateException)
assertEquals("No handler for method getIntWithArg(123)", exception2.message)
}
@Test
fun `should set int value`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
registry.setReturnValue({ mock.getInt() }, 123)
assertEquals(123, mock.getInt())
}
@Test
fun `should set string value`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
registry.setReturnValue({ mock.getString() }, "ABC")
assertEquals("ABC", mock.getString())
}
@Test
fun `should set int value with arg`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
registry.setReturnValue({ mock.getIntWithArg(123) }, 456)
registry.setReturnValue({ mock.getIntWithArg(456) }, 789)
assertEquals(456, mock.getIntWithArg(123))
assertEquals(789, mock.getIntWithArg(456))
}
@Test
fun `should set constant int body`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
registry.setBody({ mock.getInt() }) { 123 }
assertEquals(123, mock.getInt())
}
@Test
fun `should set int body`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
var counter = 0
registry.setBody({ mock.getInt() }) { counter++ }
assertEquals(0, mock.getInt())
assertEquals(1, mock.getInt())
assertEquals(2, mock.getInt())
assertEquals(3, mock.getInt())
assertEquals(4, mock.getInt())
assertEquals(5, mock.getInt())
}
@Test
fun `should set constant string body`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
registry.setBody({ mock.getString() }) { "ABC" }
assertEquals("ABC", mock.getString())
}
@Test
fun `should set string body`() {
val registry = MockRegistry()
val mock = registry.mock<GetValue>()
var counter = 0
registry.setBody({ mock.getString() }) { "ABC${counter++}" }
assertEquals("ABC0", mock.getString())
assertEquals("ABC1", mock.getString())
assertEquals("ABC2", mock.getString())
assertEquals("ABC3", mock.getString())
assertEquals("ABC4", mock.getString())
assertEquals("ABC5", mock.getString())
}
@Test
fun `should set body depending on another mock`() {
val registry = MockRegistry()
val userRepository = registry.mock<UserRepository>()
val userService = registry.mock<UserService>()
registry.setReturnValue(
{ userRepository.getUser("alex") },
User("Alex Smith")
)
registry.setReturnValue(
{ userRepository.getUser("bell") },
User("Bell Rogers")
)
registry.setReturnValue(
{ userRepository.getUser("dan") },
null
)
registry.setBody({ userRepository.allUsers() }) {
listOf(User("James Bond"), User("Jane Doe"))
}
registry.setBody({ userService.getUser() }) {
User(userRepository.getUser("dan")?.name ?: "Unknown")
}
assertEquals(User("Alex Smith"), userRepository.getUser("alex"))
assertEquals(listOf(User("James Bond"), User("Jane Doe")), userRepository.allUsers())
assertEquals(User("Bell Rogers"), userRepository.getUser("bell"))
assertEquals(User("Unknown"), userService.getUser())
registry.setReturnValue(
{ userRepository.getUser("dan") },
User("Dan Brown")
)
assertEquals(User("Dan Brown"), userService.getUser())
}
}
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.