Software Development Engineer @ Amazon
reniowood at gmail.com resume
액터를 테스트하기 위해 ScalaTest를 사용해 테스트를 작성한다.
Actor를 테스트하기 더 어려운 이유
아카는 액터 테스트를 훨씬 쉽게 만들어주는 akka-testkit 모듈을 제공한다. 해당 모듈을 이용해 다음과 같은 테스트를 수행할 수 있다.
TestKit을 사용하는 다중 스레드 스타일이 프로덕션에서 실행할 코드의 실제 환경과 가장 가까울 것이다.
액터를 세 가지 기준으로 나누어 테스트 할 수 있다.
단일 스레드 테스트
object TestActor {
case class Message(data: String)
case class GetState(receiver: ActorRef)
}
class TestActor extends Actor {
import TestActor._
val internalState = Vector[String]() // 외부에서 확인 불가
def receive = {
case Message(data) => internalState = internalState :+ data
case GetState(receiver) => receiver ! internalState
}
def state = internalState
}
class ActorTest01 extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
"단일 스레드 환경에서 메시지를 받으면 내부 상태를 변경한다" in {
import TestActor._
val testActor = TestActorRef[TestActor]
testActor ! Message("메시지 전송")
testActor.underlyingActor.state must (contains("메시지 전송"))
}
}
다중 스레드 테스트
class ActorTest02 extends TestKit(ActorSystem("testSystem"))
with WordSpecLike
with MustMatchers
with StopSystemAfterAll {
"다중 스레드 환경에서 메시지를 받으면 내부 상태를 변경한다" in {
import TestActor._
val actor = system.actorOf(Props[TestActor], "testActor")
actor ! Message("첫번째 메시지 전송")
actor ! Message("두번째 메시지 전송")
actor ! GetState(testActor) // TestKit의 testActor가 메시지를 받게 한다.
expectMsg(Vector("첫번째 메시지 전송", "두번째 메시지 전송"))
}
}
테스트 작성
"메시지를 받아 전송하는 액터" must {
"메시지를 받아 처리해서 다른 액터에게 메시지를 보낸다" in {
import SortingActor._
val props = SortingActor.props(testActor)
val sortingActor = system.actorOf(props, "sortingActor")
// 정렬되지 않은 리스트를 전송한다.
val unsortedList = (0 until 10000).map { _ =>
Random.nextInt(100000)
}.toVector
sortingActor ! Sort(unsortedList)
// expectMsgPF 함수를 사용해 정확하게 매치할 수 없는 결과를 확인한다.
expectMsgPF() {
case Sorted(sortedList) =>
sortedList.size must be(size)
unsortedList.sorted must be (sortedList)
}
}
}
받은 메시지를 처리해 다른 액터에게 결과를 담은 메시지를 보내는 SortingActor를 구현한다.
object SortingActor {
def props(receiver: ActorRef) = Props(new SortingActor(receiver))
case class Sort(unsortedList: Vector[Int])
case class Sorted(sortedList: Vector[Int])
}
class SortingActor(receiver: ActorRef) extends Actor {
import SortingActor._
def receive = {
case Sort(unsortedList) => receiver ! Sorted(unsortedList.sorted)
}
}
액터가 보낸 메시지의 수나 순서도 테스트할 수 있다.
테스트 작성
"receiveWhile과 expectNoMsg를 이용해 짝수일 때만 메시지를 보내는지 확인한다" in {
import FilteringActor._
val props = FilteringActor.props(testActor)
val filter = system.actorOf(props, "filter")
filter ! Number(1)
filter ! Number(2)
filter ! Number(3)
filter ! Number(4)
filter ! Number(5)
filter ! Number(6)
val numbers = receiveWhile() {
case Number(number) if number < 6 => number
}
numbers numst be(List(2, 4))
expectMsg(Number(6))
filter ! Number(1)
expectNoMsg
filter ! Number(2)
expectMsg(Number(2))
filter ! Number(3)
expectNoMsg
filter ! Number(4)
expectMsg(Number(4))
}
액터 구현
object FilteringActor {
def props(nextActor: ActorRef) = Props(new FilteringActor(nextActor))
case class Number(number: Long)
}
class FilteringActor(nextActor: ActorRef) extends Actor {
import FilteringActor._
def receive = {
case msg: Number =>
if (msg.number % 2 == 0) {
nextActor ! msg
}
}
}
둘 이상의 testActor가 필요한 경우에는 TestProbe 클래스를 사용한다.
액터가 akka.logger
를 이용해 로그를 남기는 지 테스트한다.
테스트 작성
object LoggingActorTest {
val testSystem = {
// ConfigFactory는 문자열을 파싱해서 설정 정보를 얻을 수 있다.
val config = ConfigFactory.parseString(
// TestEventListener는 로그에 기록되는 모든 이벤트를 처리할 수 있다.
"""
akka.loggers = [akka.testkit.TestEventListener]
"""
)
}
}
import LoggingActorTest._
class LoggingActorTest extends TestKit(testSystem) with WordSpecLike with StopSystemAfterAll {
"LoggingActor" must {
"로그에 남길 메시지를 보내면 로그로 남긴다" in {
// 로그 이벤트를 검사하기 위해 단일 스레드 환경에서 테스트를 실행한다.
val dispatcherId = CallingThreadDispatcher.id
val props = Props[Greeter].withDispatcher(dispatcherId)
val loggingActor = system.actorOf(props)
EventFilter.info(message = "[LOG] 로그 테스트", occurances = 1).intercept {
loggingActor ! Log("로그 테스트")
}
}
}
}
액터 구현
object LoggingActor {
case class Log(message: String)
}
class LoggingActor extends Actor with ActorLogging {
def receive = {
case Log(message) => log.info(s"[LOG] $message")
}
}
보낸 메시지에 대해 송신자에게 응답하는 액터가 있다면, 이를 테스트하기 위해 ImplicitSender
트레이트를 사용하면 굳이 송신자를 테스트 액터로 바꾸어줄 필요가 없다.