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