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 트레이트를 사용하면 굳이 송신자를 테스트 액터로 바꾸어줄 필요가 없다.