如何Mock Akka Actor进行单元测试?验证EventPublisher消息调用次数
没问题!针对你这个EventPublisher类的单元测试需求,我来给你详细讲讲怎么验证publish方法里给RabbitMQ Actor发送消息的次数刚好是一次。首先先把你的原代码格式化一下方便查看:
@Singleton class EventPublisher @Inject() (@Named("rabbit-mq-event-update-actor") rabbitControlActor: ActorRef) (implicit ctx: ExecutionContext) { def publish(event: Event): Unit = { logger.info("Publishing Event: {}", toJsObject(event), routingKey) rabbitControlActor ! Message.topic(shipmentStatusUpdate, routingKey = "XXX") } }
我们可以用两种常用的方式来实现这个测试:Akka TestKit的TestProbe(更贴合Akka生态)或者Mockito(通用mock工具),下面分别说明:
方法一:用Akka TestProbe做测试
TestProbe是Akka官方提供的测试工具,它可以模拟一个Actor,捕获所有发送给它的消息,非常适合测试Actor之间的交互。
1. 先准备依赖(以SBT为例)
确保你的build.sbt里添加了这些测试依赖:
libraryDependencies ++= Seq( "com.typesafe.akka" %% "akka-testkit" % "2.6.20" % Test, "org.scalatest" %% "scalatest" % "3.2.15" % Test )
2. 编写测试类
import akka.actor.ActorSystem import akka.testkit.TestKit import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import scala.concurrent.ExecutionContext class EventPublisherSpec extends TestKit(ActorSystem("EventPublisherSpec")) with AnyWordSpecLike with Matchers with BeforeAndAfterAll { // 测试结束后关闭ActorSystem,避免资源泄漏 override def afterAll(): Unit = { TestKit.shutdownActorSystem(system) } "EventPublisher" should { "send exactly one Message.topic to rabbitControlActor when publish is called" in { // 创建TestProbe来代替真实的rabbitControlActor val rabbitTestProbe = TestProbe() // 用系统的dispatcher作为隐式ExecutionContext implicit val ec: ExecutionContext = system.dispatcher // 实例化EventPublisher,把probe的ActorRef传进去 val eventPublisher = new EventPublisher(rabbitTestProbe.ref) // 构造一个测试用的Event实例(根据你的Event接口/类实现具体逻辑) val testEvent = new Event { /* 这里可以模拟Event的字段或方法实现 */ } // 调用要测试的publish方法 eventPublisher.publish(testEvent) // 验证probe收到了且仅收到一次目标消息 // 方式1:直接等待并匹配消息 rabbitTestProbe.expectMsg(Message.topic(shipmentStatusUpdate, routingKey = "XXX")) // 方式2:确认消息数量为1(更严谨) val receivedMessages = rabbitTestProbe.receiveN(1) receivedMessages should contain only Message.topic(shipmentStatusUpdate, routingKey = "XXX") } } }
方法二:用Mockito做测试
如果你更习惯用Mockito来mock对象,也可以用这种方式,核心是mockActorRef并验证它的消息发送方法被调用的次数。
1. 添加Mockito依赖
在build.sbt里加上:
libraryDependencies += "org.mockito" %% "mockito-scala" % "1.17.12" % Test
2. 编写测试类
import org.mockito.Mockito._ import org.scalatest.BeforeAndAfterEach import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.matchers.should.Matchers import org.mockito.ArgumentMatchers._ import scala.concurrent.ExecutionContext class EventPublisherMockitoSpec extends AnyWordSpecLike with Matchers with BeforeAndAfterEach { // mock一个ActorRef实例 private val mockRabbitActor = mock[ActorRef] // 用全局的ExecutionContext private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global // 每次测试前重置mock,避免测试用例之间的干扰 override def beforeEach(): Unit = { reset(mockRabbitActor) } "EventPublisher" should { "send Message.topic exactly once to rabbitControlActor" in { val eventPublisher = new EventPublisher(mockRabbitActor) val testEvent = new Event { /* 实现测试用的Event实例 */ } // 调用publish方法 eventPublisher.publish(testEvent) // 验证mockActor的!方法被调用了一次,且参数是指定的Message.topic verify(mockRabbitActor, times(1)).!(eq(Message.topic(shipmentStatusUpdate, routingKey = "XXX")))(any[ActorRef]) } } }
几个注意点
- 确保
shipmentStatusUpdate和Event的测试实例能正确构造,如果这些是外部依赖,建议mock它们来隔离测试逻辑 - 如果
toJsObject方法有复杂业务逻辑,也可以mock它,避免测试被无关逻辑干扰 - 测试时可以调整日志级别(比如设置为WARN),避免打印不必要的日志信息
内容的提问来源于stack exchange,提问作者Arun Gupta
相关产品推荐
相关产品推荐

