Skip to content

Instantly share code, notes, and snippets.

@lgtout
Last active December 17, 2021 22:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lgtout/2be831462785671845c283696e9fdc0f to your computer and use it in GitHub Desktop.
Save lgtout/2be831462785671845c283696e9fdc0f to your computer and use it in GitHub Desktop.
Eliminate indirection due to interfaces while preserving stubbing and enforcing a narrow API surface
@file:Suppress("UNREACHABLE_CODE")
package com.lagostout.extensions
interface SystemUnderTest {
companion object
}
//fun SystemUnderTest.f1(): String = "f1" + f2() // Test this first without testing f2
//fun SystemUnderTest.f2(): String = "f2" // Test this second without testing f1
interface C {
fun SystemUnderTest.f1(): String
fun SystemUnderTest.f2(): String
}
// Unfortunately, for testC3 to pass, we need to own C1, so we can make it "open".
open class C1 : C {
override fun SystemUnderTest.f1() = "f1" + f2()
override fun SystemUnderTest.f2() = "f2" // Pretend this is a slow call e.g. over HTTP.
}
//val c2 = object : ...
class C2(c1: C1) : C by c1 {
override fun SystemUnderTest.f2() = "f2-mocked"
}
class C3 : C1() {
override fun SystemUnderTest.f2() = "f2-mocked"
}
fun main() {
// testC2()
// testC3()
testD2()
// testD5()
}
fun testC2() {
val sut: SystemUnderTest = object : SystemUnderTest {}
val c1 = C1()
val c2 = C2(c1)
val r = c2.run {
sut.f1()
}
println(r) // Success!! "f1f2-mocked"
}
fun testC3() {
val sut: SystemUnderTest = object : SystemUnderTest {}
val c3 = C3()
val r = c3.run {
sut.f1()
}
println(r) // Fail!! "f1f2"
}
interface D1 {
fun SystemUnderTest.f1() = "f1" + f2()
// 1. Can we mock/stub this?
// 2. Can we exclude it from a public API?
fun SystemUnderTest.f2() = "f2"
}
object D2 : D1 {
override fun SystemUnderTest.f2() = "f2-mocked"
}
fun testD2() {
val sut: SystemUnderTest = object : SystemUnderTest {}
val d2 = D2
val r = d2.run {
sut.f1()
}
println(r) // Success!! "f1f2-mocked"
}
class D3(private val d1: D1) {
private val sut = object : SystemUnderTest {}
fun f3() = with (d1) {
sut.f1()
sut.f2() // F*&%! How can we hide f2?
}
}
interface D4 {
fun SystemUnderTest.f1(): String
}
class D5(private val d4: D4) {
private val sut = object : SystemUnderTest {}
fun f3() = with (d4) {
sut.f1()
// sut.f2() // Compiler error!! f2 is hidden!
}
}
// ACHIEVED
// We now know how we can mock/stub an extension function without introducing a
// layer of indirection that multiplies the surface area of the code and slows
// us down as we try to navigate and understand it - Provide the extension function's
// implementation directly in the interface, instead of in a concrete implementation
// of the interface.
// BUT...
// By doing this, we've lost the ability to hide from clients of the API, any extension
// functions that shouldn't be part of the interface's public API. Can we remedy this?
// Option 1: Use delegation to hide extensions that shouldn't be
// exposed in the public API
class D6(d1: D1) : D4, D1 by d1
fun testD5() {
val sut = object : SystemUnderTest {}
val d1 = object : D1 {}
val d6: D4 = D6(d1)
val r = with (d6) {
sut.f1()
// sut.f2() // Success!! Compiler error!! f2 is hidden!
}
println(r)
}
// Option 2: Use member visibility to hide the extension function that should be excluded
// from the public API
interface D7 {
fun SystemUnderTest.f1() = "f1" + f2()
// 1. Can we mock/stub this?
// 2. Can we exclude it from a public API?
// Option 2.a: Make f2 private
private fun SystemUnderTest.f2() = "f2"
// Option 2.b: Can we use protected?
//protected fun SystemUnderTest.f2() = "f2" // F@#$%!! protected cannot be used in an interface!
}
fun testD7() {
val sut = object : SystemUnderTest {}
val d1 = object : D7 {}
with (d1) {
sut.f1() // Okay
//sut.f2() // Success!! Compiler error!! f2 is hidden!
}
}
// Oh, oh! We can use private to exclude extension function from public API, but we lose the
// ability to stub.
object D8 : D7 {
// override fun SystemUnderTest.f2() = "f2-mocked" // F@#$%!! Cannot mock/stub f2! Compiler error!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment