julian
06/28/2020, 5:33 PMraulraja
06/28/2020, 7:38 PMraulraja
06/28/2020, 7:38 PMjulian
06/28/2020, 9:06 PMclass A
class B
class C
fun A.f(): C = B().f()
fun B.f(): C = C()
Above, the extension function B.f()
is an implicit dependency of A.f()
. When A.f()
is a large function with many such dependencies on other extension functions that are available in declaration scope, it makes testing more difficult. Because one can't simply unit test A.f()
by providing different arguments to A.f()
. A.f()
has no declared arguments (besides the implicit receiver).
#2
fun A.f2(bf: B.() -> C): C = B().bf()
Here, A.f2()
is a higher order function whose function dependencies are explicitly defined. These dependencies are immediately and easily identifiable when writing unit tests. We can provide an implementation of B.() -> C
that's different from what we use in production, in order to setup the specific conditions under which we want to test A.f2()
.
I see a lot of #1 in Kotlin code, but never #2.
#2 is noisy and becomes a nuisance because we must provide all these function arguments whenever we make a call.
Perhaps in that case they can be grouped together, as in #3.
#3
interface K {
fun B.f(): C = TODO()
fun C.g(): D = TODO()
}
fun A.f3(k: K): C = with (k) {
B().f()
C().g()
C()
}
julian
06/28/2020, 9:11 PMinterface J {
fun B.f(): C = C()
}
interface H : J {
fun A.f(): C = B().f()
}
// In our tests of A.f() we can override B.f() to provide some alternative C that's specific to the use case we want to verify.
val h = object : H {
override fun B.f(): C = TODO()
}
// Uses our override
fun test(): C = with (h) {
A().f()
B().f()
}
julian
06/28/2020, 9:39 PMA.f()
and we end up with an A.(E) -> D
that's easier to use and pass around.
#5
class A
class B
class C
class D
class E
fun B.f(): C = TODO()
fun C.f(): D = TODO()
fun A.f(bf: B.() -> C, cf: C.() -> D, e: E): D = TODO()
val af: A.(E) -> D = A::f.partially2(B::f).partially2(C::f)
val d = A().af(E())
julian
06/28/2020, 9:41 PMjulian
06/28/2020, 9:49 PMraulraja
06/29/2020, 12:36 AMraulraja
06/29/2020, 12:36 AMraulraja
06/29/2020, 12:37 AMraulraja
06/29/2020, 12:38 AMraulraja
06/29/2020, 12:39 AMraulraja
06/29/2020, 12:39 AMraulraja
06/29/2020, 12:39 AMraulraja
06/29/2020, 12:39 AMraulraja
06/29/2020, 12:40 AMraulraja
06/29/2020, 12:41 AMraulraja
06/29/2020, 12:57 AMraulraja
06/29/2020, 12:57 AMraulraja
06/29/2020, 12:58 AMjulian
06/29/2020, 3:39 AMDefaultUI
doesn't depend on InMemoryData
. Rather it depends on Data
. A Data
implementation of InMemoryData
is provided at the edge of the program.
So this is dependency between interfaces/classes. Whereas I'm interested in dependency within a single interface/class. In the gist the unit of dependency is the interface/class. The unit of dependency I'm asking about is the function. Though it's all on the topic of dependency, the scale of what I'm asking about is more granular.
I'll use the gist to try to express myself more clearly. Suppose DefaultDomain
looked like this:
class DefaultDomain : Domain {
fun Int.roundUpOrDown() = TODO()
override fun <R> R.getProcessedAccount(): IO<Account> where R: Data =
fetchAccount().map { Account(it.balance.roundUpOrDown()) }
}
Now, you want to unit test DefaultDomain R.getProcessedAccount()
. But you want to avoid actually calling the real implementation of DefaultDomain Int.roundUpOrDown()
. Maybe because of overhead. Maybe because you want to test roundUpOrDown
separately.
You will write two tests of getProcessedAccount()
, one where the balance is rounded up. And another when the balance is rounded down. To do this you need to provide an alternate implementation of roundUpOrDown
. This implementation would return a hardcoded rounded up number in the first case, and a hardcoded rounded down number in the second case.
What's the best way to do this?