Akka Actor 测试参考指南

引言

Akka Actor的测试主要依赖ActorTestKit及其衍生的测试工具,并以ScalaTest为主要测试套件。

本文从Akka Platform Guide的测试代码中简单整理了Akka Actor测试相关的一些内容,供编写测试时参考。

ScalaTestWithActorTestKit 🔗

ScalaTestWithActorTestKit是测试的基础,用于模拟一个完整的ActorSystem。该测试套件将会在测试通过时自动关闭,或者是在测试失败后使用trait BeforeAndAfterAll手动关闭。

ScalaTestWithActorTestKit默认将加载application-test.conf中的配置,或者使用Akka自带的reference.conf,而不会是项目中使用的application.conf,所以通常需要自己定义配置的加载位置。

BehaviorTestKit 🔗

BehaviorTestKit是用于同步测试的工具(异步测试应使用ActorTestKit),可以用它创建子Actor、监测另一个Actor、检测发生Effect的情况,是最基础的Actor测试工具。

// Get the child inbox for the child ActorRef, or fail if there is no such child.
abstract def childInbox[U](child: typed.ActorRef[U]): TestInbox[U]

// Get the child inbox for the child with the given name, or fail if there is no child with the given name spawned
abstract def childInbox[U](name: String): TestInbox[U]

// Get the akka.actor.typed.Behavior testkit for the given child akka.actor.typed.ActorRef.
abstract def childTestKit[U](child: typed.ActorRef[U]): BehaviorTestKit[U]

// Clear the log entries
abstract def clearLog(): Unit

// The current behavior, can change any time run is called
abstract def currentBehavior: Behavior[T]

// Asserts that the oldest effect is the expectedEffect.
abstract def expectEffect(expectedEffect: Effect): Unit

// Asserts that the oldest effect matches the given partial function.
abstract def expectEffectPF[R](f: PartialFunction[Effect, R]): R

// Asserts that the oldest effect is of type T.
abstract def expectEffectType[E <: Effect](implicit classTag: ClassTag[E]): E

// Returns if there have been any effects.
abstract def hasEffects(): Boolean

// Is the current behavior alive or stopped
abstract def isAlive: Boolean

// Returns all the CapturedLogEvent issued by this behavior(s)
abstract def logEntries(): Seq[CapturedLogEvent]

// The receptionist inbox contains messages sent to system.receptionist
abstract def receptionistInbox(): TestInbox[Command]

// Requests all the effects.
abstract def retrieveAllEffects(): Seq[Effect]

// Requests the oldest Effect or akka.actor.testkit.typed.Effect.NoEffects if no effects have taken place.
abstract def retrieveEffect(): Effect

// Returns the current behavior as it was returned from processing the previous message.
abstract def returnedBehavior: Behavior[T]

// Send the message to the behavior and record any Effects
abstract def run(message: T): Unit

// Send the first message in the selfInbox to the behavior and run it, recording Effects.
abstract def runOne(): Unit

// The self inbox contains messages the behavior sent to context.self
abstract def selfInbox(): TestInbox[T]

// Send the signal to the behavior and record any Effects
abstract def signal(signal: Signal): Unit

EventSourcedBehaviorTestKit 🔗

EventSourcedBehaviorTestKit是对应于EventSourcedBehavior的测试工具,可以测试command触发的event,还可以测试event被触发后产生的state的变化情况,并获得执行command后回复的reply。

@ApiMayChange
@DoNotInherit trait EventSourcedBehaviorTestKit[Command, Event, State] {

  import EventSourcedBehaviorTestKit._

  /**
   * Run one command through the behavior. The returned result contains emitted events and the state
   * after applying the events.
   */
  def runCommand(command: Command): CommandResult[Command, Event, State]

  /**
   * Run one command  with a `replyTo: ActorRef[R]` through the behavior. The returned result contains emitted events,
   * the state after applying the events, and the reply.
   */
  def runCommand[R](creator: ActorRef[R] => Command): CommandResultWithReply[Command, Event, State, R]

  /**
   * Retrieve the current state of the Behavior.
   */
  def getState(): State

  /**
   * Restart the behavior, which will then recover from stored snapshot and events. Can be used for testing
   * that the recovery is correct.
   */
  def restart(): RestartResult[State]

  /**
   * Clears the in-memory journal and snapshot storage and restarts the behavior.
   */
  def clear(): Unit

  /**
   * The underlying `PersistenceTestKit` for the in-memory journal and snapshot storage.
   * Can be useful for advanced testing scenarios, such as simulating failures or
   * populating the journal with events that are used for replay.
   */
  def persistenceTestKit: PersistenceTestKit
}

CommandResultWithReply 🔗

CommandResultCommandResultWithReply是BehaviorTestKit执行runCommand后的结果。

// The command that was run.
def command: Command

// The first event.
def event: Event

// The first event as a given expected type.
def eventOfType[E <: Event](eventClass: Class[E]): E

// The events that were emitted by the command, and persisted.
def events: List[Event]

// true if no events were emitted by the command.
def hasNoEvents: Boolean

// The reply. It will throw AssertionError if there was no reply.
def reply: Reply

// The reply as a given expected type.
def replyOfType[R <: Reply](replyClass: Class[R]): R

// The state after applying the events.
def state: State

// The state as a given expected type.
def stateOfType[S <: State](stateClass: Class[S]): S

模板示例

import akka.pattern.StatusReply

object ShoppingCartSpec {
  val config = ConfigFactory
    .parseString(
      """
      akka.actor.serialization-bindings {
        "shopping.cart.CborSerializable" = jackson-cbor
      }
      """)
    .withFallback(EventSourcedBehaviorTestKit.config)
}

class ShoppingCartSpec extends ScalaTestWithActorTestKit(ShoppingCartSpec.config)
  with AnyWordSpecLike {

  val cartId = "testCart"
  val entity = EventSourcedBehaviorTestKit[Command, Event, State](system, ShoppingCart(cartId, "carts-0"))

  val result: CommandResultWithReply = entity.runCommand[StatusReply[Summary]](AddItem("foo", 42, _))
  val result: CommandResultWithReply = entity.runCommand[StatusReply[Summary]](replyTo => AddItem("foo", 42, replyTo))

  result.reply.isSucess should ===(true)
  result.reply.isError should ===(true)
  result.reply should ===(StatusReply.Success(Summary(Map("foo" -> 42), checkedOut = false)))

  result.event should ===(ItemAdded(cartId, "foo", 42))
  result.event.asInstanceOf[CheckedOut].cartId should ===(cartId)
}


val sharedConfig: Config = ConfigFactory.load("integration-test.conf")
val testKit = ActorTestKit("IntegrationSpec", nodeConfig(grpcPort, managementPorts, managementPortIndex)
  .withFallback(sharedConfig)
  .resolve())

def system: ActorSystem[_] = testKit.system

val testNode = new TestNodeFixture(grpcPorts(0), managementPorts, 0)
val orderServiceProbe = testNode.testKit.createTestProbe[OrderRequest]()
val orderRequest = orderServiceProbe.expectMessageType[OrderRequest]

orderRequest.cartId should ===("cart-2")
orderRequest.items.head.itemId should ===("bar")
orderRequest.items.head.quantity should ===(18)
posted @ 2021-04-16 18:43  没头脑的老毕  阅读(338)  评论(0编辑  收藏  举报