Hi all, I am having a hard time trying to switch m...
# arrow
i
Hi all, I am having a hard time trying to switch my OOP testing style using mocks to FP with extension functions and receivers as implicit dependencies. Using @raulraja gist https://gist.github.com/raulraja/e82e352c10f7e158758ff48aca733d3c let’s say we want to implement a different behaviour in a testing scope for
fun RedisConnection.saveRedisSession(session: Session): Save<Unit>
How can we achieve that? I understand that this is a impossible approach because extension functions are resolved statically. Then, how to mock o test double them? Or the real problem is we should not need to mock functions used by other functions? Then what to test? 😄 Sorry for the mess but switching paradigms is a tough brain task. Thank you very much in advance.
p
I can give you a hand about this
The key is to wrap abstractions into an interface + default methods
that's not different from your regular OOP
and you have interfaces depending on interfaces
so far so goo
the upside of interfaces is that they don't easily allow internal state
yes you can add a getter, but it's much easier to pass state as a parameter to the functions
the second problem is when there may be a point where you depend on a concretion
that's usually where hard mocking frameworks step in
but mocking is a red herring, what you want is to convert that hard dependency into a soft one
via depending on an interface or a lambda
Copy code
fun IService.doThing(s: UserState) =
  HardDependency.mangleUser(this.stuff(s))
becomes
Copy code
fun IService.doThingIface(soft: IHardDep, u: UserState) =
  soft.mangleUser(stuff(u))
or
Copy code
fun IService.doThingLambda(
  s: UserState, 
  f: (UserState) -> UserState) =
f(stuff(s))
"omg but that's been on Java since forever!" yes, we kind of forgot about it, or didn't have the convenience of lambdas
Copy code
object TestService: IService {
  
override fun stuff(user: UserState): UserState =
   user.apply { name = "hello" }

}
...

val result = TestService.doThingLambda(someState) { user->
  user.apply { name = name.toUpper() }
}

expect(result.name).toEqual("HELLO")
and it's fractal: you can apply this at all levels
you only end up with concretions at the setup phase of your program or test
i
Really interesting approach @pakoito. Then I understand you end losing
implicit dependencies
as receivers because you switch style to inject them as function parameters. Is that correct?
p
there are several things I can understand your question relates to
with classes you have implicit parameters in the sense that methods aren't granular, so to mock a method that uses a field A you may need to mock the whole class and all fields
that problem goes away
the other one is how many extfun receivers you can have
which is just one...for now
so it's up to you what you'd make the receiver, or whether you group them in a Tuple or in a data class or interface
that grouping is so annoying that you might as well go back to regular parameters
classic java here
fun bla(/* 20 parameters */)
refactors to
fun bla(a: IFace1 /* 5 parameters */, b: IFace2 /* 4 parameters */...)
etc
i
My question was related to this style of FP in Kotlin:
Copy code
suspend fun Dependencies.loadNetworkSpeakers(): List<Speaker> =
  speakerService.loadSpeakers() // access dependencies

suspend fun Dependencies.loadNetworkTalks(speakerIds: List<SpeakerId>): List<Talk> =
  talkService.loadTalks(speakerIds) // access dependencies

suspend fun Dependencies.persistTalks(talks: List<Talk>): List<Talk> =
  talkDatabase.persistTalks(talks) // access dependencies

abstract class Dependencies {
  val speakerService: SpeakerServiceOps by lazy { SpeakerService() }
  val talkService: TalkServiceOps by lazy { TalkService() }
  val talkDatabase: TalkDatabaseOps by lazy { TalkDatabase() }
}
As you can see, you have at hand all the dependencies in an extfun.
p
yes
what is the question then, sorry
i
In a testing scope, how can I mock behaviour of, let’s say
Dependencies.persistTalks(talks: List<Talk>): List<Talk>
?
p
pass a lambda to the function that uses it
Copy code
fun Dependencies.doThing(talks: List<Talk>) =
  improveTalks(talks)
  .let(::persistTalks)
  .let(::logTalks)
that's with hard dependencies
Copy code
fun Dependencies.doThing(talks: List<Talk>, persist: (List<Talk>) -> List<Talk>, log: (List<Talk>) -> List<Talk>) =
  improveTalks(talks)
  .let(::persist)
  .let(::log)
that's with soft dependencies
and you can test the function in isolation
i
I understand now. You’ve been very kind. Thank you very much @pakoito I get the idea now. I will explore this approach.
p
Copy code
interface TalkActions {
  fun persist(l: List<Talk>): List<Talk>
  fun log(l: List<Talk>): List<Talk>
}
^^ for interface grouping, as those lambdas can be functions or fields 😄
i
Nice 👍
p
Copy code
fun Dependencies.doThing(talks: List<Talk>, actions: TalkActions) =
  improveTalks(talks)
  .let(actions::persist)
  .let(actions::log)
Copy code
fun TalkActions.doThing(talks: List<Talk>, d: Dependencies) =
  d.improveTalks(talks)
  .let(::persist)
  .let(::log)
flipped even
at this point is up to your choice 😄
🙌 1