You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用withObjectMocked模拟的方法在Actor调用时未生效求助

问题描述

将无状态组件从类重构为Scala object后,使用mockito-scala的withObjectMocked功能模拟外部服务连接,直接调用方法时模拟正常,但引入Akka Actor后,Actor会绕过模拟调用真实实现。

目标是模拟FooService.doit()方法(真实实现抛出RuntimeException,测试中禁止执行):

object FooService {
  def doit(): String = {
    // 测试中不应执行此逻辑
    throw new RuntimeException("executed real impl!!!")
  }
}

FooService.doit()仅由FooActor.handleDoit()调用,该方法可通过接收Doit消息或直接调用触发:

object FooActor {
  val outcome: Promise[Try[String]] = Promise[Try[String]]()

  case object Doit

  def apply(): Behavior[Doit.type] = Behaviors.receiveMessage { _ =>
    handleDoit()
    Behaviors.same
  }

  def handleDoit(): Unit = {
    try {
      val res = FooService.doit()
      outcome.success(Success(res))
    } catch {
      case ex: RuntimeException =>
        outcome.success(Failure(ex))
    }
  }
}

使用withObjectMocked模拟FooService.doit(),所有测试代码都在该块内,并通过Await等待FooActor.outcome的结果:

withObjectMocked[FooService.type] {
  FooService.doit() returns "mock result"
  // 执行测试场景
}

设置两种测试场景:

  1. 直接调用:模拟正常工作
def simpleSetup(): Try[String] = {
  FooActor.handleDoit()
  Await.result(FooActor.outcome.future, 1.seconds)
}

输出:finished with result: mock result

  1. 通过Actor触发:模拟失效,调用真实实现
def actorSetup(): Try[String] = {
  val system: ActorSystem[FooActor.Doit.type] = ActorSystem(FooActor(), "FooSystem")
  system ! FooActor.Doit
  val result = Await.result(FooActor.outcome.future, 1.seconds)
  system.terminate()
  result
}

输出:failed with exception: executed real impl!!!

原因分析

mockito-scala的withObjectMocked基于**线程局部存储(Thread-Local Storage)**实现,仅在创建mock的线程中生效。而Akka Actor运行在独立的线程池线程中,与测试代码所在线程并非同一个,因此Actor线程无法感知测试线程中创建的mock,最终调用了真实的FooService.doit()实现。

解决方案

方案1:将FooService改为可注入的依赖

把FooService从object改为class,通过构造函数注入到Actor中,测试时直接传入mock实例,彻底绕过object的静态特性限制:

// 重构FooService为class
class FooService {
  def doit(): String = {
    throw new RuntimeException("executed real impl!!!")
  }
}

// 重构FooActor,通过构造函数接收FooService依赖
object FooActor {
  val outcome: Promise[Try[String]] = Promise[Try[String]]()
  case object Doit

  def apply(fooService: FooService): Behavior[Doit.type] = Behaviors.receiveMessage { _ =>
    handleDoit(fooService)
    Behaviors.same
  }

  def handleDoit(fooService: FooService): Unit = {
    try {
      val res = fooService.doit()
      outcome.success(Success(res))
    } catch {
      case ex: RuntimeException =>
        outcome.success(Failure(ex))
    }
  }
}

// 测试代码
val mockFooService = mock[FooService]
mockFooService.doit() returns "mock result"

val system = ActorSystem(FooActor(mockFooService), "FooSystem")
system ! FooActor.Doit
val result = Await.result(FooActor.outcome.future, 1.seconds)
system.terminate()

方案2:使用Akka TestKit规范异步测试

同时去掉全局outcome Promise,改用TestProbe接收Actor结果,让测试更符合Akka异步风格:

// 重构FooActor,通过sender传递结果
object FooActor {
  case object Doit
  case class DoitResult(result: Try[String])

  def apply(fooService: FooService): Behavior[Doit.type] = Behaviors.receive { (context, msg) =>
    val res = Try(fooService.doit())
    context.sender() ! DoitResult(res)
    Behaviors.same
  }
}

// 测试代码(基于Akka TestKit)
class FooActorSpec extends AnyWordSpec with TestKit with ImplicitSender with BeforeAndAfterAll {
  override def afterAll(): Unit = TestKit.shutdownActorSystem(system)

  "FooActor" should {
    "use mocked FooService when triggered" in {
      val mockFooService = mock[FooService]
      mockFooService.doit() returns "mock result"

      val actor = system.actorOf(FooActor(mockFooService).props())
      actor ! FooActor.Doit

      val result = expectMsgType[FooActor.DoitResult]
      result.result shouldBe Success("mock result")
    }
  }
}

方案3:临时线程上下文传播(不推荐)

若无法重构object,可尝试手动将mock上下文传播到Actor线程,但该方式依赖mockito-scala内部实现,稳定性较差:

withObjectMocked[FooService.type] {
  FooService.doit() returns "mock result"
  val mockContext = MockitoScalaSession.getCurrentSession.get

  val system = ActorSystem(Behaviors.receiveMessage[FooActor.Doit.type] { _ =>
    // 在Actor线程激活mock上下文
    MockitoScalaSession.withSession(mockContext) {
      FooActor.handleDoit()
    }
    Behaviors.same
  }, "FooSystem")
  
  system ! FooActor.Doit
  val result = Await.result(FooActor.outcome.future, 1.seconds)
  system.terminate()
}
总结

最可靠的方案是方案1,通过依赖注入将静态object改为可注入class,从根本上解决mock的线程限制问题,同时让代码更符合依赖倒置原则,提升可测试性。方案2进一步优化了测试的异步风格,更适配Akka应用的测试实践。

内容的提问来源于stack exchange,提问作者Zwackelmann

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 11:01:01