akka 在'withObjectMocked'中创建的模拟方法在从'Actor'调用时不会被调用

q3qa4bjr  于 2022-11-23  发布在  其他
关注(0)|答案(1)|浏览(144)

我已经在https://github.com/Zwackelmann/mockito-actor-test上发布了一个展示我的问题的最小项目
在我的项目中,我将几个组件从类重构为对象,在所有情况下,类都没有真正有意义的状态。由于其中一些对象建立了到需要模拟的外部服务的连接,我很高兴地看到mockito-scala引入了withObjectMocked上下文函数,它允许在函数范围内模拟对象。
这个特性对我来说非常有效,直到我在混合中引入了Actor,尽管在withObjectMocked上下文中,它会忽略被模拟的函数。
为了进一步解释我所做的,请查看上面的github示例项目,它已经准备好通过sbt run执行。
我的目标是模拟下面的doit函数。它不应该在测试过程中被调用,所以对于这个演示,它只是抛出一个RuntimeException

object FooService {
  def doit(): String = {
    // I don't want this to be executed in my tests
    throw new RuntimeException(f"executed real impl!!!")
  }
}

FooService.doit函数只能从FooActor.handleDoit函数调用,该函数由FooActor在收到Doit消息后调用或直接调用。

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))
    }
  }
}

为了模拟Foo.doit,我使用了withObjectMocked,如下所示。下面的所有代码都在这个块中。为了确保块不会由于异步执行而离开,我Await返回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"
  }
  // [...]
}

我现在有两个测试设置:第一个函数直接调用FooActor.handleDoit

def simpleSetup(): Try[String] = {
  FooActor.handleDoit()
  val result: Try[String] = Await.result(FooActor.outcome.future, 1.seconds)
  result
}

第二个设置通过Actor触发FooActor.handleDoit

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
}

两种设置都等待outcome承诺完成,然后退出块。
通过在simpleSetupactorSetup之间切换,我可以测试这两种行为。由于这两种行为都是在withObjectMocked上下文中执行的,我希望这两种行为都触发模拟函数。但是actorSetup忽略模拟函数并调用真实的的方法。

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!!!

有什么建议吗?

kpbpu008

kpbpu0081#

withObjectMock依赖于在与withObjectMock相同的线程中执行模拟的代码(请参见Mockito的实现和ThreadAwareMockHandler对当前线程的检查)。
由于参与者在ActorSystem的调度程序的线程上执行(从不在调用线程中执行),因此他们看不到这样的模拟。
您可能希望使用BehaviorTestKit来测试参与者,BehaviorTestKit本身有效地使用了ActorContextActorSystem的mock/stub实现,BehaviorTestKit的一个示例封装了一个行为,并向其传递在测试线程中同步处理的消息(通过runrunOne方法)。请注意,BehaviorTestKit有一些限制:某些类别的行为实际上无法通过BehaviorTestKit进行测试。
更广泛地说,我倾向于认为,在 akka 语中,嘲笑是不值得的:如果您需要普遍的mock,那就是实现不佳的标志。ActorRef(尤其是类型化的mock)是IMO的终极mock:将需要模拟的内容用自己的协议封装到自己的参与者中,并将ActorRef注入到被测行为中,然后验证被测行为是否正确地支持了协议。但是如果你想/需要花费精力来提高这些覆盖率...)你可以像上面一样使用BehaviorTestKit技巧(因为行为所做的唯一事情就是执行模拟功能,它几乎肯定不会属于BehaviorTestKit无法测试的行为类别)。

相关问题