LoginSignup
44
40

More than 5 years have passed since last update.

テストしないで大丈夫? AkkaのActorを単体テストする

Last updated at Posted at 2014-03-27

はじめに

この記事では、AkkaのActorを用いたプログラムの単体テスト方法について簡単に説明します。

Actorをテストするのは通常のオブジェクトをテストするよりも下記の理由により困難です。

  1. タイミング
    メッセージは非同期に送信されるので、単体テストでいつ検査を実行したらよいかを知ることは難しいことです。

  2. 非同期
    Actorは並列のスレッドで実行されます。これはシングルスレッドで実行されるプログラムよりも検査するのが難しいです。

  3. ステートレス
    Actorは内部の状態を隠蔽しています。通常この内部状態に直接アクセスすることはできないので、単体テストが困難になります。

  4. 強調と結合
    複数のActorと結合して実行されます。これをテストするにはActor間のメッセージのやりとりを盗み聞きする必要があります。

この面倒なテストを、Akka-TestKitを使うことによって軽減します。

Akka-TestKit

Akka-TestKitによって下記のテストが可能になります。

  1. シングルスレッド単体テスト
    Actorインスタンスは通常直接アクセスできません。testkitはTestActorRefを提供していて、Actorインスタンスへのアクセスができるようになります。これによって、Actorに定義されているメソッドを呼びだすことができます。シングルスレッドで実行することにより、通常のオブジェクトと同様にテストを記述できます。

  2. マルチスレッド単体テスト
    testkitは同様に、TestKitと、TestProbeクラスを提供しています。TestProbeを使うと、Actorからの返信をインスペクトすることができます。TestKitクラスは、期待しているメッセージかどうかを確かめることができます。Actorは通常のマルチスレッド環境で実行されます。

  3. 複数JVMテスト
    Akkaは同様に複数のJVMのテストも可能にします。これは、リモートのアクターシステムをより簡単にテストできるようにする仕組みです。

TestActorRef

TestActorRefは同期テストを可能にします、通常Actorのインスタンスは取得できませんが、TestActorRefを用いるとインスタンスが取得できます。

import akka.testkit.TestActorRef

val actorRef = TestActorRef[MyActor]
val actor = actorRef.underlyingActor

サンプルコード

必要インポート

import org.scalatest.{ Suite, BeforeAndAfterAll, WordSpecLike, MustMatchers }
import akka.testkit.{ TestKit, TestActorRef }
import akka.actor.{ Props, ActorSystem, Actor, ActorRef }

アクターシステムをテスト後に停止するトレイト

SBTなどでテストを実行すると、アクターは動作しつづけてしまうので、テスト後に停止するようにします。

trait StopSystemAfterAll extends BeforeAndAfterAll {
  this: TestKit with Suite =>
  override protected def afterAll() {
    super.afterAll()
    system.shutdown()
  }
}

プロトコル

ActorではActorとのメッセージのやりとりをプロトコルとしてグループ化しておく習慣があるようです。

object ActorProtocol {
  case class Message(data: String)
  case class GetState(receiver: ActorRef)
}

アクター

varを使っていますが、アクターは同時に動作することはないのでロックなどは気にしなくても問題ありません。

class MessageActor extends Actor {
  import ActorProtocol._

  var internalState = Vector[String]()

  def receive = {
    case Message(data) => internalState = internalState :+ data
    case GetState(receiver)  => receiver ! internalState
  }

  def state = internalState
}

シングルスレッドのスペック

TestKitをextendsすることにより、testActorやexpectMsgアサーションなどの機能が使えるようになります。

class ActorSpec extends TestKit(ActorSystem("test"))
    with WordSpecLike
    with MustMatchers
    with StopSystemAfterAll {

  "A actor" must {
    "change state when it receives a message" in {
      import ActorProtocol._

      val actor = TestActorRef[MessageActor]

      actor ! Message("ハロー")
      actor.underlyingActor.state must (contain("ハロー"))
    }
  }
}

マルチスレッドのスペック

testActorをアクターに渡すことにより、testActorで受け取ったメッセージを、expectMsgで確認することができるようになります。

class ActorSpec extends TestKit(ActorSystem("test"))
    with WordSpecLike
    with MustMatchers
    with StopSystemAfterAll {

  "A actor" must {
    "change state when it receives a message, multi threaded" in {
      import ActorProtocol._

      val actor = system.actorOf(Props[MessageActor], "s3")

      actor ! Message("ハロー")
      actor ! Message("はろ〜")

      actor ! GetState(testActor)

      expectMsg(Vector("ハロー", "はろ〜"))
    }
  }
}

送信されたメッセージを取得する

val message = receiveOne(1 second)

リスナーでメッセージを受け取る

class Sender(listener: Option[ActorRef] = None) extends Actor {
  def receive = {
    case Send(msg) => 
      listener.foreach { _ ! msg }
  }
}
val sender = system.actorOf(Props(Sender(testActor)))
sender ! Message("test")
receiveOne(1 second)

おわりに

TestKitでは、expectMsgの様なアクターをテストするための便利なメソッドが多数用意されていますので詳しいことはScalaDocを参照して下さい。http://doc.akka.io/api/akka/2.0/akka/testkit/TestKit.html

44
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
40