1. 概要

Akkaは、JVM上でリアクティブ、並行、および分散アプリケーションを構築するための便利なフレームワークまたはツールキットです。 これはリアクティブマニフェストに基づいているため、イベント駆動型で、復元力があり、スケーラブルで、応答性があります。

アクターは、Akkaの基本的な構成要素であり、メッセージを交換することによってのみ通信できる、メモリフットプリントが小さい小さな処理ユニットを表します。

このチュートリアルでは、アクターをテストして、期待どおりに動作することを確認する方法を見ていきます。

2. テスト構成

Akkaチームが推奨する通常のクラシックActorsの代わりに、Akkaタイプを使用します。

テストスイートを設定するには、テストの依存関係を build.sbt:に追加する必要があります。

val AkkaVersion = "2.6.16"
libraryDependencies += "com.typesafe.akka" %% "akka-actor-testkit-typed" % AkkaVersion % Test

libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.4" % Test

AkkaTestKit は、Akkaチームが推奨するScalaTestと一緒に使用するのが最適です。

AkkaTestKit は、以下へのアクセスを提供するActorTestKitのインスタンスを作成します。

  • ActorSystem
  • アクターを生成するためのメソッド。
  • テストスイートからActorSystemをシャットダウンする方法

テストキットを提供する簡単なテスト構成は次のとおりです。

class TestService
  extends AnyWordSpec
    with BeforeAndAfterAll
    with Matchers {
  val testKit = ActorTestKit()
  implicit val system = testKit.system 
  override def afterAll(): Unit = testKit.shutdownTestKit()
}

これで、クラスを拡張してテストの作成を開始できます。 また、ActorSystemのシャットダウンを担当するafterAllメソッドをオーバーライドする必要がありました。

3. テストパターン

あいさつを送信する単純なあいさつActorプログラムを設計し、Actorが次のように単純に応答するとします。

object Greeter {
  case class Sent(greeting: String, self: ActorRef[Received])
  case class Received(greeting: String)

  def apply(): Behavior[Sent] = Behaviors.receiveMessage {
    case Sent(greeting, recipient) =>
      recipient ! Received(greeting)
      Behaviors.same
  }
}

testkitを使用してこのActorを生成し、送信したのと同じメッセージを受信することを確認することで、この単純なプログラムをテストできます。

testkit は、テストプローブを提供します。 このプローブは、テスト中のActorからメッセージを受信します。 このプローブは、不要なメッセージが入らないようにします。

class GreeterTest extends TestService {
  import scala.concurrent.duration._
  val greeting = "Hello there"
  val sender = testKit.spawn(Greeter(), "greeter")
  val probe = testKit.createTestProbe[Greeter.GreetingResponse]()
  sender ! Greeter.GreetingRequest(greeting, probe.ref)
  probe.expectMessage(Greeter.GreetingResponse(greeting))
  probe.expectNoMessage(50 millis)
}

ここでは、入力された Actor を表すプローブを使用して、予期されたメッセージだけを確実に受信できるようにします。

テストプローブは基本的に、 Actor、の代わりに使用できるクエリ可能なメールボックスであり、受信したメッセージをアサートできます。

より複雑な例として、 Actor を使用して信号機システムを実装し、状態が常に保持されて正しいことを確認しましょう。

object TrafficLight {
  sealed trait Signal
  object Signal {
    case object RED extends Signal
    case object YELLOW extends Signal
    case object GREEN extends Signal
  }

  sealed trait SignalCommand 
  object SignalCommand {
    case class ChangeSignal(recipient : ActorRef[CurrentSignal]) extends SignalCommand
    case class GetSignal (recipient : ActorRef[CurrentSignal]) extends SignalCommand
  }
  case class CurrentSignal(signal : Signal)

  import Signal._
  def apply() : Behavior[SignalCommand] = Behaviors.setup{_ =>
    var state : Signal = RED
    Behaviors.receiveMessage {
      case ChangeSignal(recipient) =>
       val nextState =  state match {
          case RED => YELLOW
          case YELLOW => GREEN
          case GREEN => RED

        }
        state = nextState
        recipient ! CurrentSignal(nextState)
        Behaviors.same

      case GetSignal(recipient) =>
        recipient ! CurrentSignal(state)
        Behaviors.same
        
    }
  }
}

この単純なActorでは、初期状態は REDです。受信したメッセージに応じて、状態を変更したり、現在の状態を返したりできます。 このActorをテストして、期待どおりに状態が変化することを確認する必要があります。 また、後で他のメッセージを受信しないことをテストする必要があります。

class TrafficLightTest extends TestService {
  import scala.concurrent.duration._
  val sender = testKit.spawn(TrafficLight(), "traffic")
  val probe = testKit.createTestProbe[TrafficLight.CurrentSignal]()

  sender ! TrafficLight.SignalCommand.GetSignal(probe.ref)
  probe.expectMessage(TrafficLight.CurrentSignal(TrafficLight.Signal.RED))
  probe.expectNoMessage(50 millis)

  sender ! TrafficLight.SignalCommand.ChangeSignal(probe.ref)
  probe.expectMessage(TrafficLight.CurrentSignal(TrafficLight.Signal.YELLOW))
  probe.expectNoMessage(50 millis)

  sender ! TrafficLight.SignalCommand.ChangeSignal(probe.ref)
  probe.expectMessage(TrafficLight.CurrentSignal(TrafficLight.Signal.GREEN))
  probe.expectNoMessage(50 millis)
  
  sender ! TrafficLight.SignalCommand.ChangeSignal(probe.ref)
  probe.expectMessage(TrafficLight.CurrentSignal(TrafficLight.Signal.RED))
  probe.expectNoMessage(50 millis)
}

このテストでは、Actorが期待どおりに状態を変更していることがわかります。

アクター:質問パターンをテストすることもできます

class TrafficLightTestFut extends TestService {
  import scala.concurrent.duration._
  val sender = testKit.spawn(TrafficLight(), "traffic")
  val duration = 300 millis
  implicit val timeout = Timeout(duration)
  
  val signalFut =  sender.ask( replyTo => TrafficLight.SignalCommand.GetSignal(replyTo) )
  val signal = Await.result(signalFut, duration)
  assert(signal == CurrentSignal(RED))
}

トラフィックActorは現在の信号を照会され、正しい信号を返します。

4. 結論

この記事では、型指定された Actor テスト構成をセットアップする方法と、単純なテストパターンを記述して観察する方法について説明しました。 テストを作成する方法はたくさんあります。テストがどのように機能するかについての基本的な基礎と構成要素を理解する必要があります。

いつものように、ソースコードはGitHubにあります。