Akka実践バイブルをゆっくり読み解く企画の第3章です。
第3章 アクターによるテスト駆動開発
3章にしていきなりテストです。
2017年冬に『テスト駆動開発』を読んでテストへの意識が最高潮にあるので楽しみな反面、そもそもテスト対象のAkkaについて全然理解してなくて不安しかない。いや、むしろテスト対象のことを完全に理解できていないからテストをやるのか!
アクターのテスト
アクターはテストについて、以下のような性質を持っている。これらは一見、テストを容易にしてくれる性質のように見える。
- アクターは振る舞いを持つため、テストの対象としやすい。
- 通常の単体テストではインタフェース/機能を別々に(もしくはインタフェースのみ)テストするが、振る舞いをテストすることによってインタフェース/機能の両方をテストすることができる。
- メッセージを送信することで振る舞いを用意にシミュレートできるため、テストしやすい。
とは言っても、アクターのテストは通常の場合よりも困難になる。
なぜかと言うと、以下のような性質を持つことを合わせて考えながらテストしなければならないから。
- タイミング
- 非同期
- ステートレス
- 協調・統合
■タイミング
メッセージが非同期であることを踏まえて、いつアサートするのか?
■非同期
マルチスレッドのテストは難しい。不用意なbarrierはテスト全体の進行をブロックしてしまう可能性がある。
■ステートレス
アクターは内部状態を隠蔽しているため、外部から内部状態をテストすることができない。どうやって状態の確認を行うのか?
■協調・統合
アクター間の統合テストを行う場合、アクター間のメッセージやり取りを盗み見て期待とおりであるかをテストする必要がある。
アクターの良いところが見事にテストを困難にしている。
これらを踏まえてテストを実践していく。
Akkaでのテスト
上で「アクターのテストは難しい」と言ったばかりだけど、Akkaではakka-testkitモジュールが提供されており、これによってテストが簡単にできるようになっている。このモジュールを使うことで、以下のようなテストができる。
- シングルスレットの単体テスト
- マルチスレッドの単体テスト
- マルチJVMのテスト(詳細は6章で)
ここから先は実際のテストコードを見ながら進めていくけれど、その前に「同じ操作を不必要に繰り返さない」ための準備を行う。
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
シングルスレッドである場合とマルチスレッドである場合で、若干実装が異なってくる。
まずは、以下のようなシングルスレッドなアクターを想定する。
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
。
これに対して、以下のようなテストを行う。
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"))
}
}
}
次に、マルチスレッド版。シングルスレッド版と異なる箇所には★マークを付けている。
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
}
これに対するテストは以下のとおり。
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
を使うことで内部状態をテストに送信することができるようになる。また、expectMsg
でtestActor
の受信メッセージを確認することができる。
これって、内部状態を知りたければ業務的には不要でもGetState
に相当するメッセージを持たせておかなければならないってことなんだろうか?ちょっとモヤっとする・・・
SendingActor
次にSendingActorのテストを確認する。ここで確認したいことは、アクターが処理を完了した後に別のアクターに送るメッセージの内容の妥当性。
SendingActorの種類について
SendingActorにもバリエーションがある。書籍内では以下が紹介されている。SendingActorなので、いずれも受け取ったメッセージを元に別のアクターに何らかのメッセージを送信する性質を持っている。
アクター | 説明 |
---|---|
MutatingCopyActor | このアクターは変更したコピーを作成し、それを次のアクターに送信する。下記のSortEventsはこれに該当する。 |
ForwardingActor | このアクターメッセージの変更を行わず、受け取ったメッセージを転送する。 |
TransformingActor | このアクターは受け取ったメッセージから別のメッセージを作る。 |
FilteringActor | このアクターは受け取って不要なものを破棄したメッセージを転送する。下記のFilteringActorはこれに該当する。 |
SequencingActor | このアクターは受け取った1つうのメッセージから複数のメッセージを作成し、新しいメッセージを順番にそれぞれ別のアクターを送信する。 |
#### MutatingCopyActor まずはテスト対象のアクターから確認する。
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))
}
}
これに対するテストは以下のとおり。
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)))
で解決する気がするんだけど・・・
と思って調べてみたら、やっぱりこれでも良いっぽい。
expectMsg
とexpectMsgPF
のシグネチャを見てみると以下のようになっている。
def expectMsg[T](obj: T): T
def expectMsgPF[T](max: Duration = Duration.Undefined, hint: String = "")(f: PartialFunction[Any, T]): T
expectMsg
がT型のオブジェクトを引数にとるのに対して、expectMsgPF
はAny => T
の部分関数を引数にとる。つまり、expectMsg
ではSendingActorのテストのように複数の観点での確認(件数の確認とVectorの確認)を行うことができない。expectMsg
であれば部分関数としてテスト内容を定義することができるため、複数の観点での確認ができるようになる。
書籍のこの部分については、このようなテスト方法もあるよという紹介にすぎないと解釈した。
#### 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
}
}
}
}
これに対するテストは以下のとおり。
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))
}
}
}
このテストは以下のような書き方をすることもできる。
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のテストを確認する。ここで確認したいことは、アクターによる副作用。
早速テスト対象のコードを見てみる。
case class Greeting(message: String)
class Greeter extends Actor with ActorLogging {
def receive = {
// メッセージを受信すると、コンソールに挨拶を出力する
case Greeting(message) => log.info("Hello {}!", message)
}
}
これに対するテストは以下のとおり。
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)
}
}
Greeter
はakka.actor.ActorLogging
トレイトを使ってログメッセージを書き込んでおり、今回はその内容をテストする。TestEventListner
を設定すると、ログ出力イベントを制御することができるようになるため、これにより副作用の確認を行う。ちなみに、今回はログレベル:INFOに対して「Hello World!」という文言でフィルタをかけている。
上記ケースは以下のような書き方をすることもできる。
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)
}
}
これに対するテストは以下のとおり。
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)
に変更する方法が紹介されている。
まずはテスト対象のコードから。
class EchoActor extends Actor {
def receive = {
case msg =>
// 受け取ったメッセージをそのまま暗黙的なsenderに送信する
sender() ! msg
}
}
これに対するテストは以下のとおり。
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)が準備されているため、こいつらを使いこなして正しく動くアクターを作っていかないといけない。