Search code examples
scalaunit-testingmockitoakkamockito-scala

Mocked methods created within `withObjectMocked` not invoked when called from an `Actor`


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 Actors 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?


Solution

  • 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).