Akka에서 테스트 코드를 작성하는 법을 정리하려고 한다.
다음과 같은 3개의 액터가 있다고 생각하자.
class SimpleActor extends Actor {
override def receive: Receive = {
case message => sender() ! message
}
}
class Blackhole extends Actor {
override def receive: Receive = {
case _ => Actor.emptyBehavior
}
}
import scala.util.Random
class LabTestActor extends Actor {
private val random = new Random()
override def receive:Receive = {
case "greeting" =>
if(random.nextBoolean()) sender() ! "hi"
else sender() ! "hello"
case "favoriteTech" =>
sender() ! "Akka"
sender() ! "Scala"
case message:String => sender() ! message.toUpperCase()
}
}
위의 3개 액터에 대한 테스트 코드를 작성하고자 한다. 이때, asynchronous
한 테스트로 작성되는 것을 유념하자.
기초적인 테스트 클래스이므로 BasicSpec
이라는 클래스 명으로 작성할 예정이다.
테스트 클래스를 명명할 때 Spec
이라는 접미사를 붙여 테스트 클래스인 것을 알 수 있도록 한다.
class BasicSpec extends TestKit(ActorSystem("testSystem")
with ImplicitSender
with WordSpecLike
with BeforeAndAfterAll
위와 같이 3개의 trait를 믹스인하며, Testkit을 상속해야 한다.
TestKit(ActorSystem)
은 테스트를 위한 환경을 설정하는 것이다. 테스트가 시작되면 파라미터로 주어진 ActorSystem이 시작되면서 테스트 환경이 만들어진다.
우리는 액터에 대한 테스트를 할 때 메시지를 보내고 받을 것이다. 따라서 메시지를 보내고 받는 액터가 있어야 할 것이다. ImplicitSender
trait를 사용한다면 별 다른 액터 선언 없이도 메시지를 주고 받은 후 테스트할 수 있다.
natural language로 테스트를 작성하는 것이 가능하다.
"description" should {
"test scenario" in {
// 실제 테스트 내용
}
}
위와 같이 인간 친화적인 언어로 테스트를 구성할 수 있도록 한다.
우리는 TestKit(ActorSystem)
을 extends하면서 테스트 환경을 구축했었다. 하지만, AcotrSystem은 여러 개의 스레드를 다루는 무거운 data structure이므로 테스트가 종료되고 나면 ActorSystem을 shutdown하기 위한 trait이다.
위의 BeforeAndAfterAll
trait를 믹스인 한 다음 내부에서 밑의 메서드를 오버라이드 시켜주면 테스트 후 자동으로 ActorSystem이 종료된다.
override def afterAll():Unit = {
TestKit.shutdownActorSystem(system)
}
우리는 위쪽에서 3개의 액터를 테스트하려고 한다. 따라서, 하나 씩 시나리오를 작성해볼 것이다.
class SimpleActor extends Actor {
override def receive: Receive = {
case message => sender() ! message
}
}
위 쪽에 이미 작성한 액터 코드이지만 다시 가져왔다.
SimpleActor
는 받은 메시지를 그대로 다시 발신자에게 보내는 단순한 액터이다. 우리는 따라서 SimpleActor
에게 어떠한 메시지를 보냈으면 되돌아오는 메시지 역시 보냈던 메시지와 일치해야 한다.
"SimpleActor가 보내는 메시지는" should {
"보냈던 메시지와 일치해야 한다." in {
val simpleActor = system.actorOf(Props[SimpleActor])
val message = "test message"
simpleActor ! message
expectMsg(message)
}
}
위의 코드를 실행시키면 제대로 결과가 나오는 것을 볼 수 있다.
즉, expectMsg
를 통해서 액터가 응답하는 메시지가 올바른지 여부를 판단할 수 있는 것이다.
"SimpleActor가 보내는 메시지는" should {
"assert를 사용해 확인해도 보냈던 메시지와 일치해야 한다." in {
val simpleActor = system.actorOf(Props[SimpleActor])
val message = "test message"
simpleActor ! message
val result = expectMsgType[String]
assert(result == message)
}
}
사실 assert만을 사용하는 것이 아니라 expectMsgType[type]
을 사용하여 예상되는 응답의 타입을 넣어 해당 응답을 불러온다. 그리고, 실제 액터가 보낸 응답인 result
와 예상되는 응답인 message
가 같은지 assert
메서드를 사용하여 확인하는 것이다.
class Blackhole extends Actor {
override def receive: Receive = {
case _ => Actor.emptyBehavior
}
}
위와 같이 Blackhole 액터는 어떠한 메시지를 받아도 아무런 응답을 보내지 않는 액터이다. 따라서 우리는 액터가 어떠한 응답도 보내지 않는다는 테스트를 작성해야 한다. 이를 위해서는 expectNoMsg(duration)
을 사용해야 한다. duration
에는 scala.concurrent.duration._
패키지에 있는 값이 들어온다.
필자는 확실하게 아무런 메시지를 받지 않는다는 것을 확인하기 위해 10초 동안 기다리는 테스트 코드를 작성했다.
"Blackhole 액터는 " should {
import scala.concurrent.duration._
"어떠한 응답도 보내지 않아야 한다." in {
val blackholeActor = system.actorOf(Props[Blackhole])
val message = "hello, blackhole!"
blackholeActor ! message
expectNoMsg(10.second)
}
}
위의 테스트 코드는 10초 동안 기다린 후 테스트를 해보았다.
10초가 지나도 어떠한 응답도 오지 않아 테스트가 통과된 것을 확인할 수 있다.
import scala.util.Random
class LabTestActor extends Actor {
private val random = new Random()
override def receive:Receive = {
case "greeting" =>
if(random.nextBoolean()) sender() ! "hi"
else sender() ! "hello"
case "favoriteTech" =>
sender() ! "Akka"
sender() ! "Scala"
case message:String => sender() ! message.toUpperCase()
}
}
이제는 조금은 복잡한 LabTestActor를 테스트할 예정이다.
LabTestActor는 크게 3가지로 받을 수 있는 응답이 나뉜다.
1. "greeting"이라는 String을 받았을 때 ➡️ "hi"나 "hello" 중 하나가 랜덤으로 발송된다.
2. "favoriteTech"라는 String을 받았을 때 ➡️ "Akka"와 "Scala"라는 2개의 메시지가 응답으로 발송
3. 위의 2개 외의 String이 입력되었을 때 ➡️ 모든 영어 소문자를 대문자로 바꾸어 발송
"lab test actor는" should {
"greeting이라는 메시지를 받았을 때 hi나 hello를 응답해야 한다." in {
val labTestActor = system.actorOf(Props[LabTestActor])
val MESSAGE = "greeting"
labTestActor ! MESSAGE
expectMsgAnyOf("hi", "hello")
}
}
expectMsgAnyOf(나올 수 있는 옵션들)
이라는 메서드를 사용하면 랜덤으로 응답해도 옵션들 중에 해당 메시지가 있다면 테스트를 통과한다.
"lab test actor는" should {
"favoriteTech라는 메시지를 받았을 때 Scala와 Akka 2개의 메시지를 응답해야 한다." in {
val labTestActor = system.actorOf(Props[LabTestActor])
val MESSAGE = "favoriteTech"
labTestActor ! MESSAGE
expectMsgAllOf("Akka", "Scala")
}
}
여러 개의 응답이 오는 경우 expectMsgAllOf(받는 모든 메시지)
를 입력해야 한다.
그렇다면 위와 같이 통과가 되는 것을 알 수 있다.
"lab test actor는" should {
"favoriteTech라는 메시지를 받았을 때 expectMsgPF로 scala와 akka를 받아야 한다." in {
val labTestActor = system.actorOf(Props[LabTestActor])
val MESSAGE = "favoriteTech"
labTestActor ! MESSAGE
expectMsgPF() {
case "Scala" =>
case "Akka" =>
}
}
}
expectMsgPF
를 사용하면 스칼라의 강력한 기능 중 하나인 패턴 매칭을 사용할 수 있다. 이때, 굳이 무슨 행위를 정의하지 않아도 case에 받을 수 있는 모든 응답을 입력하는 것이다.
그렇게 된다면 위와 같이 모두 통과하는 것을 알 수 있다.