5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Akka実践バイブルをゆっくり読み解く 第3章アクターによるテスト駆動開発

Posted at

Akka実践バイブルをゆっくり読み解く企画の第3章です。

第3章 アクターによるテスト駆動開発

3章にしていきなりテストです。
2017年冬に『テスト駆動開発』を読んでテストへの意識が最高潮にあるので楽しみな反面、そもそもテスト対象のAkkaについて全然理解してなくて不安しかない。いや、むしろテスト対象のことを完全に理解できていないからテストをやるのか!

アクターのテスト

アクターはテストについて、以下のような性質を持っている。これらは一見、テストを容易にしてくれる性質のように見える。

  • アクターは振る舞いを持つため、テストの対象としやすい。
  • 通常の単体テストではインタフェース/機能を別々に(もしくはインタフェースのみ)テストするが、振る舞いをテストすることによってインタフェース/機能の両方をテストすることができる。
  • メッセージを送信することで振る舞いを用意にシミュレートできるため、テストしやすい。

とは言っても、アクターのテストは通常の場合よりも困難になる。
なぜかと言うと、以下のような性質を持つことを合わせて考えながらテストしなければならないから。

  • タイミング
  • 非同期
  • ステートレス
  • 協調・統合

■タイミング

メッセージが非同期であることを踏まえて、いつアサートするのか?

■非同期

マルチスレッドのテストは難しい。不用意なbarrierはテスト全体の進行をブロックしてしまう可能性がある。

■ステートレス

アクターは内部状態を隠蔽しているため、外部から内部状態をテストすることができない。どうやって状態の確認を行うのか?

■協調・統合

アクター間の統合テストを行う場合、アクター間のメッセージやり取りを盗み見て期待とおりであるかをテストする必要がある。

アクターの良いところが見事にテストを困難にしている。
これらを踏まえてテストを実践していく。

Akkaでのテスト

上で「アクターのテストは難しい」と言ったばかりだけど、Akkaではakka-testkitモジュールが提供されており、これによってテストが簡単にできるようになっている。このモジュールを使うことで、以下のようなテストができる。

  • シングルスレットの単体テスト
  • マルチスレッドの単体テスト
  • マルチJVMのテスト(詳細は6章で)

ここから先は実際のテストコードを見ながら進めていくけれど、その前に「同じ操作を不必要に繰り返さない」ための準備を行う。

StopSystemAfterAll.scala
trait StopSystemAfterAll extends BeforeAndAfterAll { 

  // 自分型アノテーションにより、TestKitを使うテストにミックスインしている場合のみに使用可能
  this: TestKit with Suite =>
  override protected def afterAll(): Unit = {
    super.afterAll()
    // 全てのテストを実行したらあと、systemをシャットダウン
    system.terminate()
  }
}

アクターシステムは一度作成されると起動してから停止するまで動き続けるため、テスト終了時にアクターを個別に停止する手間を省くことができる。

一方向のメッセージ

tellを使ったテストを考える。tellはUnit型なので、そもそも戻り値が無い。戻り値によるテストはできないし、そもそも「タイミング」の問題を抱えている。
ここでやりたいことのポイントは2つ。

  • アクターにメッセージを送る
  • アクターが実施したであろう振る舞いの結果を確認する

「振る舞いの結果」とあるが、アクターの振る舞いには3つのバリエーションがある。

  • SilentActor
  • SendingActor
  • SideEffectingActor

■SilentActor

SilentActorについては、アクターの振る舞いを外部から直接確認することはできない(Silentっていうくらいなので・・・)。
SilentActorに対して確認したいことは以下の点。

  • 例外をスローしなかったこと
  • アクターが終了したこと
  • 内部状態の変化

これらのうち、「内部状態の変化」については akka.testkit.TestActorRef を使用することで確認することができるようになる。

■SendingActor

テスト対象のアクターが受信したメッセージの処理完了後、別のアクターにメッセージを送信する。
その際のメッセージ内容によって挙動の妥当性を確認する。

■SideEffectingActor

Side Effectは「副作用」のこと。
アクターがメッセージを処理する際に何らかのオブジェクトに対して副作用を与える。その影響内容の妥当性を確認する。

これらのアクターのバリエーションについて、テストの内容を見ていく。
テストは以下のようなテンプレートで記述される。

class テストクラス名 extends TestKit(ActorSystem("テスト対象となるアクターシシステム"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {

  "テスト対象" must {
    "テスト内容" in {
      // テストコード
      ...
    }
  }
}

上記例のようにTestKitを継承し、その際にテストで用いるアクターシステムを渡すことでアクターのテストが可能となる。
書籍ではテスト自体の説明もしてくれているけど、Akkaに焦点をあてるためにテスト自体についての説明は割愛。

SilentActor

シングルスレッドである場合とマルチスレッドである場合で、若干実装が異なってくる。
まずは、以下のようなシングルスレッドなアクターを想定する。

SilentAcor(シングルスレッド版)
object SilentActor {
  case class SilentMessage(data: String)
  case class GetState(receiver: ActorRef)
}

class SilentActor extends Actor {
  import SilentActor._

  // 内部状態がVector(イミュータブル)に格納されていく
  var internalState = Vector[String]()

  def receive = {
    case SilentMessage(data) =>
      // 内部状態を追加
      internalState = internalState :+ data
  }

  // 内部状態を返す
  def state = internalState
}

ここでのテスト対象は内部状態であるinternalState
これに対して、以下のようなテストを行う。

SilentActorTest(シングルスレッド版)
class SilentActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {

  "A Silent Actor" must {
    "change internal state when it receives a message, single" in {
      import SilentActor._

      // シングルスレッド用のTestActorRefを作成する
      val silentActor = TestActorRef[SilentActor]

      // メッセージ送信
      silentActor ! SilentMessage("whisper")

      // SilentActorが「whisper」を含んでいるかを確認する
      // underlyingActorを経由してActorの持つメソッドを呼び出せるようなので、
      // stateメソッドでinternalStateを取得している
      silentActor.underlyingActor.state must (contain("whisper"))
    }
  }
}

次に、マルチスレッド版。シングルスレッド版と異なる箇所には★マークを付けている。

SilentActor(マルチスレッド版)
object SilentActor {
  case class SilentMessage(data: String)
  case class GetState(receiver: ActorRef)
}

class SilentActor extends Actor {
  import SilentActor._
  var internalState = Vector[String]()

  def receive = {
    case SilentMessage(data) =>
      internalState = internalState :+ data

    // ★内部状態を返すためのメッセージを追加
    // パラメータにActorRefをとる
    case GetState(receiver) => receiver ! internalState
  }

  // ★内部状態を返すためのメソッドは不要
  // def state = internalState
}

これに対するテストは以下のとおり。

SilentActorTest(マルチスレッド版)
class SilentActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {

  "A Silent Actor" must {
    "change internal state when it receives a message, multi" in {
      import SilentActor._

      // ★テストキットのactorSystemを使用してテスト対象のアクターを作成する
      val silentActor = system.actorOf(Props[SilentActor], "s3")
      silentActor ! SilentMessage("whisper1")
      silentActor ! SilentMessage("whisper2")

      // ★内部状態はTestKitが持つtestActorを通して確認するため、GetStateにtestActorを渡す
      silentActor ! GetState(testActor)
      // ★Vectorの中身を確認する
      expectMsg(Vector("whisper1", "whisper2"))
    }
  }
}

マルチスレッド下ではアクターのインスタンスに直接アクセスできないが、testActorを使うことで内部状態をテストに送信することができるようになる。また、expectMsgtestActorの受信メッセージを確認することができる。
これって、内部状態を知りたければ業務的には不要でもGetStateに相当するメッセージを持たせておかなければならないってことなんだろうか?ちょっとモヤっとする・・・

SendingActor

次にSendingActorのテストを確認する。ここで確認したいことは、アクターが処理を完了した後に別のアクターに送るメッセージの内容の妥当性。

SendingActorの種類について

SendingActorにもバリエーションがある。書籍内では以下が紹介されている。SendingActorなので、いずれも受け取ったメッセージを元に別のアクターに何らかのメッセージを送信する性質を持っている。

アクター 説明
MutatingCopyActor このアクターは変更したコピーを作成し、それを次のアクターに送信する。下記のSortEventsはこれに該当する。
ForwardingActor このアクターメッセージの変更を行わず、受け取ったメッセージを転送する。
TransformingActor このアクターは受け取ったメッセージから別のメッセージを作る。
FilteringActor このアクターは受け取って不要なものを破棄したメッセージを転送する。下記のFilteringActorはこれに該当する。
SequencingActor このアクターは受け取った1つうのメッセージから複数のメッセージを作成し、新しいメッセージを順番にそれぞれ別のアクターを送信する。

#### MutatingCopyActor まずはテスト対象のアクターから確認する。
SendingActor
object SendingActor {
  // パラメータのActorRefをもとにアクターを生成する
  // テスト時はtestActorを渡す
  def props(receiver: ActorRef) = Props(new SendingActor(receiver))
  case class Event(id: Long)  
  case class SortEvents(unsorted: Vector[Event])  
  case class SortedEvents(sorted: Vector[Event])
}

// SendingActorから次に送信する先のActorはパラメータによって指定される
class SendingActor(receiver: ActorRef) extends Actor {
  import SendingActor._
  def receive = {
    // 未ソート状態のVectorを受け取って、ID順にソートしたVectorを指定されたアクターに送信する
    case SortEvents(unsorted) =>
      receiver ! SortedEvents(unsorted.sortBy(_.id))
  }
}

これに対するテストは以下のとおり。

SendingActorTest
class SendingActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {

  "A Sending Actor" must {
    "send a message to another actor when it has finished processing" in {
      import SendingActor._

      // testActorをパラメータにSendingActorアクターを生成する
      val props = SendingActor.props(testActor) 
      val sendingActor = system.actorOf(props, "sendingActor")
      
      val size = 1000
      val maxInclusive = 100000

      def randomEvents() = (0 until size).map{ _ => 
        Event(Random.nextInt(maxInclusive))
      }.toVector

      // 未ソート状態のVectorを生成する
      val unsorted = randomEvents()
      // ソートを行う
      val sortEvents = SortEvents(unsorted)
      sendingActor ! sortEvents

      expectMsgPF() {
        case SortedEvents(events) =>
          // 要素数に過不足がないことを確認する
          events.size must be(size)
          // testActorに送られたVectorがソート済であることを確認する
          unsorted.sortBy(_.id) must be(events)
      }
    }
  }
}

ここで結果確認にexpectMsgPFが使われている。これについて書籍内では『SortEventsメッメージはランダムなイベントのベクターを持っているため、expectMsg(mgs)を使うことは出来ません』とあるが、これが一見よくわからない。
expectMsg(SortedEvents(unsorted.sortBy(_.id)))で解決する気がするんだけど・・・
と思って調べてみたら、やっぱりこれでも良いっぽい

expectMsgexpectMsgPFのシグネチャを見てみると以下のようになっている。

expectMsg
def expectMsg[T](obj: T): T
expectMsgPF
def expectMsgPF[T](max: Duration = Duration.Undefined, hint: String = "")(f: PartialFunction[Any, T]): T

expectMsgがT型のオブジェクトを引数にとるのに対して、expectMsgPFAny => Tの部分関数を引数にとる。つまり、expectMsgではSendingActorのテストのように複数の観点での確認(件数の確認とVectorの確認)を行うことができない。expectMsgであれば部分関数としてテスト内容を定義することができるため、複数の観点での確認ができるようになる。
書籍のこの部分については、このようなテスト方法もあるよという紹介にすぎないと解釈した。


#### FilteringActor まずはテスト対象のアクターから確認する。
FilteringActor
object FilteringActor {
  def props(nextActor: ActorRef, bufferSize: Int) = Props(new FilteringActor(nextActor, bufferSize))
  case class Event(id: Long)
}

class FilteringActor(nextActor: ActorRef, bufferSize: Int) extends Actor {
  import FilteringActor._
  var lastMessages = Vector[Event]()
  def receive = {
    case msg: Event =>
      if (!lastMessages.contains(msg)) {
        // Vectorに含まれないEventの場合のみ処理する

        // VectorにEventを追加
        lastMessages = lastMessages :+ msg

        // メッセージを送信
        nextActor ! msg

        if (lastMessages.size > bufferSize) {
          // 最も古いものを破棄
          lastMessages = lastMessages.tail
        }
      }
  }
}

これに対するテストは以下のとおり。

FilteringActorTest
class FilteringActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {
  "A Filtering Actor" must {

    "filter out particular messages" in {
      import FilteringActor._
      val props = FilteringActor.props(testActor, 5)
      val filter = system.actorOf(props, "filter-1")
      filter ! Event(1)  // Vector(1)
      filter ! Event(2)  // Vector(1, 2)
      filter ! Event(1)  // これは無視されるはず
      filter ! Event(3)  // Vector(1, 2, 3)
      filter ! Event(1)  // これは無視されるはず
      filter ! Event(4)  // Vector(1, 2, 3, 4)
      filter ! Event(5)  // Vector(1, 2, 3, 4, 5)
      filter ! Event(5)  // これは無視されるはず
      filter ! Event(6)  // Vector(2, 3, 4, 5, 6) バッファが5なので、先頭から切り捨てられる

      // if id <= 5 にマッチしている間繰り返す
      // 今回の例であれば、id = 6 の時に繰り返しを抜ける
      val eventIds = receiveWhile() {
        case Event(id) if id <= 5 => id
      }
      eventIds must be(List(1, 2, 3, 4, 5))

      // 次に来るメッセージがEvent(6)であることを期待する
      expectMsg(Event(6))
    }
  }
}

このテストは以下のような書き方をすることもできる。

FilteringActorTest
class FilteringActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with MustMatchers
  with StopSystemAfterAll {
  "A Filtering Actor" must {

    "filter out particular messages using expectNoMsg" in {
      import FilteringActor._
      val props = FilteringActor.props(testActor, 5)
      val filter = system.actorOf(props, "filter-2")
      filter ! Event(1)
      filter ! Event(2)
      expectMsg(Event(1))
      expectMsg(Event(2))
      filter ! Event(1)
      expectNoMsg          // すぐ上のfilter ! Event(1)に対してFilteringActorがメッセージを発行しないことを期待
      filter ! Event(3)
      expectMsg(Event(3))
      filter ! Event(1)
      expectNoMsg          // すぐ上のfilter ! Event(1)に対してFilteringActorがメッセージを発行しないことを期待
      filter ! Event(4)
      filter ! Event(5)
      filter ! Event(5)
      expectMsg(Event(4))
      expectMsg(Event(5))
      expectNoMsg()          // 3行上のfilter ! Event(5)に対してFilteringActorがメッセージを発行しないことを期待
    }
  }
}

expectNoMsgはメッセージが発信されないことをタイムアウトによって確認するため、テストの実行に時間がかかる。


#### TestKitのtestActorについて TestKitは`testActor`を1つしか持っていない。SequencingActorのように複数のActorへメッセージを送信するようなActorをテストする場合は`testActor`が1つでは足りない。この場合は、TestKitではなくTestProbeというクラスを使用する。ここではTestProbeの使い方は説明されておらず、紹介のみとなっている。今後頻繁に出てくるようなので、アタマの片隅に置いておこう。

SideEffectingActor

次にSideEffectingActorのテストを確認する。ここで確認したいことは、アクターによる副作用。
早速テスト対象のコードを見てみる。

Greeter
case class Greeting(message: String)

class Greeter extends Actor with ActorLogging {
  def receive = {
    // メッセージを受信すると、コンソールに挨拶を出力する
    case Greeting(message) => log.info("Hello {}!", message)
  }
}

これに対するテストは以下のとおり。

Greeter01Test
class Greeter01Test extends TestKit(testSystem)
  with WordSpecLike
  with StopSystemAfterAll {

  "The Greeter" must {
    "say Hello World! when a Greeting(\"World\") is sent to it" in {
      // 現スレッドのIDを取得し、同一スレッドにアクターを生成する
      val dispatcherId = CallingThreadDispatcher.Id
      val props = Props[Greeter].withDispatcher(dispatcherId)
      val greeter = system.actorOf(props)
      EventFilter.info(message = "Hello World!", occurrences = 1).
        intercept {
          greeter ! Greeting("World")
        }
    }
  }
}

object Greeter01Test {
  val testSystem = {
    val config = ConfigFactory.parseString(
      """
         akka.loggers = [akka.testkit.TestEventListener]
      """)
    ActorSystem("testsystem", config)
  }
}

Greeterakka.actor.ActorLoggingトレイトを使ってログメッセージを書き込んでおり、今回はその内容をテストする。TestEventListnerを設定すると、ログ出力イベントを制御することができるようになるため、これにより副作用の確認を行う。ちなみに、今回はログレベル:INFOに対して「Hello World!」という文言でフィルタをかけている。

上記ケースは以下のような書き方をすることもできる。

Greeter02
object Greeter02 {
  // メッセージが正常にログ出力された場合にSome(_)が渡ってくる
  def props(listener: Option[ActorRef] = None) = Props(new Greeter02(listener))
}
class Greeter02(listener: Option[ActorRef])
  extends Actor with ActorLogging {
  def receive = {
    case Greeting(who) =>
      val message = "Hello " + who + "!"
      log.info(message)
      // Noneの場合は実行されない
      listener.foreach(_ ! message)
  }
}

これに対するテストは以下のとおり。

Greeter02Test
class Greeter02Test extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with StopSystemAfterAll {

  "The Greeter" must {
    "say Hello World! when a Greeting(\"World\") is sent to it" in {
      val props = Greeter02.props(Some(testActor))
      val greeter = system.actorOf(props, "greeter02-1")
      greeter ! Greeting("World")
      expectMsg("Hello World!")
    }
  }
}

expectMsgが使えるので、書き味が揃えるためにもこっちの書き方の方が良いのかな?

双方向のメッセージ

『双方向メッセージはSendingActor(SilentActorの誤記と思われる)スタイルのアクターのマルチスレッド化したテストのところですでに見ています』だと!?気付かなかったけど、確かに!!!
そんなわけで、双方向のメッセージのテストは既にできるようになっているわけだけど、ここではtellが使用する暗黙のsenderをTestKitのActorRef(testActor)に変更する方法が紹介されている。

まずはテスト対象のコードから。

EchoActor
class EchoActor extends Actor {
  def receive = {
    case msg =>
      // 受け取ったメッセージをそのまま暗黙的なsenderに送信する
      sender() ! msg
  }
}

これに対するテストは以下のとおり。

EchoActorTest
class EchoActorTest extends TestKit(ActorSystem("testsystem"))
  with WordSpecLike
  with ImplicitSender  // 暗黙的なsenderをTestKitのActorRef(testActor)に設定する
  with StopSystemAfterAll {


  "An EchoActor" must {
    "Reply with the same message it receives without ask" in {
      val echo = system.actorOf(Props[EchoActor])
      echo ! "some message"
      // testActorにメッセージが送信されるので、それを確認する
      expectMsg("some message")
    }
  }
}

テストメソッドからtestActorを渡さなくてもtestActorに対してメッセージ送信させることができるようになるため、SilentActorの箇所で感じていた

これって、内部状態を知りたければ業務的には不要でもGetStateに相当するメッセージを持たせておかなければならないってことなんだろうか?ちょっとモヤっとする・・・

というモヤモヤは無事解消された!

個人的まとめ

とりあえずtestActorがスゴすぎる!
冒頭でも書かれているように、アクターのテストは難しいがゆえに、テストコードに頼らなければ十分なテストが困難だと思われる。それに対してtestActorをはじめ色々なテストツール(TestKit)が準備されているため、こいつらを使いこなして正しく動くアクターを作っていかないといけない。

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?