I have published a minimal project showcasing my problem at https://github.com/Zwackelmann/mockito-actor-test
In my project I refactored a couple of components from classes to objects in all cases where the class did not really have a meaningful state. Since some of these objects establish connections to external services that need to be mocked, I was happy to see that mockito-scala
introduced the withObjectMocked
context function, which allows mocking objects within the scope of the function.
This feature worked perfectly for me until I introduced Actor
s in the mix, which would ignore the mocked functions despite being in the withObjectMocked
context.
For an extended explanation what I did check out my github example project from above which is ready to be executed via sbt run
.
My goal is to mock the doit
function below. It should not be called during tests, so for this demonstration it simply throws a RuntimeException
.
object FooService {
def doit(): String = {
// I don't want this to be executed in my tests
throw new RuntimeException(f"executed real impl!!!")
}
}
The FooService.doit
function is only called from the FooActor.handleDoit
function. This function is called by the FooActor
after receiving the Doit
message or when invoked directly.
object FooActor {
val outcome: Promise[Try[String]] = Promise[Try[String]]()
case object Doit
def apply(): Behavior[Doit.type] = Behaviors.receiveMessage { _ =>
handleDoit()
Behaviors.same
}
// moved out actual doit behavior so I can compare calling it directly with calling it from the actor
def handleDoit(): Unit = {
try {
// invoke `FooService.doit()` if mock works correctly it should return the "mock result"
// otherwise the `RuntimeException` from the real implementation will be thrown
val res = FooService.doit()
outcome.success(Success(res))
} catch {
case ex: RuntimeException =>
outcome.success(Failure(ex))
}
}
}
To mock Foo.doit
I used withObjectMocked
as follows. All following code is within this block. To ensure that the block is not left due to asynchronous execution, I Await
the result of the FooActor.outcome
Promise.
withObjectMocked[FooService.type] {
// mock `FooService.doit()`: The real method throws a `RuntimeException` and should never be called during tests
FooService.doit() returns {
"mock result"
}
// [...]
}
I now have two test setups: The first simply calls FooActor.handleDoit
directly
def simpleSetup(): Try[String] = {
FooActor.handleDoit()
val result: Try[String] = Await.result(FooActor.outcome.future, 1.seconds)
result
}
The second setup triggers FooActor.handleDoit
via the Actor
def actorSetup(): Try[String] = {
val system: ActorSystem[FooActor.Doit.type] = ActorSystem(FooActor(), "FooSystem")
// trigger actor to call `handleDoit`
system ! FooActor.Doit
// wait for `outcome` future. The 'real' `FooService.doit` impl results in a `Failure`
val result: Try[String] = Await.result(FooActor.outcome.future, 1.seconds)
system.terminate()
result
}
Both setups wait for the outcome
promise to finish before exiting the block.
By switching between simpleSetup
and actorSetup
I can test both behaviors. Since both are executed within the withObjectMocked
context, I would expect that both trigger the mocked function. However actorSetup
ignores the mocked function and calls the real method.
val result: Try[String] = simpleSetup()
// val result: Try[String] = actorSetup()
result match {
case Success(res) => println(f"finished with result: $res")
case Failure(ex) => println(f"failed with exception: ${ex.getMessage}")
}
// simpleSetup prints: finished with result: mock result
// actorSetup prints: failed with exception: executed real impl!!!
Any suggestions?
CodePudding user response:
withObjectMock
relies on the code exercising the mock executing in the same thread as withObjectMock
(see Mockito's implementation and see ThreadAwareMockHandler
's check of the current thread).
Since actors execute on the threads of the ActorSystem
's dispatcher (never in the calling thread), they cannot see such a mock.
You may want to investigate testing your actor using the BehaviorTestKit
, which itself effectively uses a mock/stub implementation of the ActorContext
and ActorSystem
. Rather than spawning an actor, an instance of the BehaviorTestKit
encapsulates a behavior and passes it messages which are processed synchronously in the testing thread (via the run
and runOne
methods). Note that the BehaviorTestKit
has some limitations: certain categories of behaviors aren't really testable via the BehaviorTestKit
.
More broadly, I'd tend to suggest that mocking in Akka is not worth the effort: if you need pervasive mocks, that's a sign of a poor implementation. ActorRef
(especially of the typed variety) is IMO the ultimate mock: encapsulate exactly what needs to be mocked into its own actor with its own protocol and inject that ActorRef
into the behavior under test. Then you validate that the behavior under test holds up its end of the protocol correctly. If you want to validate the encapsulation (which should be as simple as possible to the extent that it's obviously correct, but if you want/need to spend effort on getting those coverage numbers up...) you can do the BehaviorTestKit
trick as above (and since the only thing the behavior is doing is exercising the mocked functionality, it almost certainly won't be in the category of behaviors which aren't testable with the BehaviorTestKit
).