[Akka] Testing 1

smlee·2023년 10월 8일
0

Akka

목록 보기
29/50
post-thumbnail

Akka에서 테스트 코드를 작성하는 법을 정리하려고 한다.

다음과 같은 3개의 액터가 있다고 생각하자.

  1. SimpleActor
class SimpleActor extends Actor {
  override def receive: Receive = {
    case message => sender() ! message
  }
}
  1. BlackholeActor
class Blackhole extends Actor {
  override def receive: Receive = {
    case _ => Actor.emptyBehavior
  }
}
  1. LabTestActor
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을 상속해야 한다.

(1) TestKit

TestKit(ActorSystem)은 테스트를 위한 환경을 설정하는 것이다. 테스트가 시작되면 파라미터로 주어진 ActorSystem이 시작되면서 테스트 환경이 만들어진다.

(2) ImplicitSender

우리는 액터에 대한 테스트를 할 때 메시지를 보내고 받을 것이다. 따라서 메시지를 보내고 받는 액터가 있어야 할 것이다. ImplicitSender trait를 사용한다면 별 다른 액터 선언 없이도 메시지를 주고 받은 후 테스트할 수 있다.

(3) WordSpecLike

natural language로 테스트를 작성하는 것이 가능하다.

"description" should {
	"test scenario" in {
    	// 실제 테스트 내용
    }
}

위와 같이 인간 친화적인 언어로 테스트를 구성할 수 있도록 한다.

(4) BeforeAndAfterAll

우리는 TestKit(ActorSystem)을 extends하면서 테스트 환경을 구축했었다. 하지만, AcotrSystem은 여러 개의 스레드를 다루는 무거운 data structure이므로 테스트가 종료되고 나면 ActorSystem을 shutdown하기 위한 trait이다.

위의 BeforeAndAfterAll trait를 믹스인 한 다음 내부에서 밑의 메서드를 오버라이드 시켜주면 테스트 후 자동으로 ActorSystem이 종료된다.

override def afterAll():Unit = {
	TestKit.shutdownActorSystem(system)
}

액터 테스트하기

우리는 위쪽에서 3개의 액터를 테스트하려고 한다. 따라서, 하나 씩 시나리오를 작성해볼 것이다.

(1) SimpleActor

class SimpleActor extends Actor {
  override def receive: Receive = {
    case message => sender() ! message
  }
}

위 쪽에 이미 작성한 액터 코드이지만 다시 가져왔다.
SimpleActor는 받은 메시지를 그대로 다시 발신자에게 보내는 단순한 액터이다. 우리는 따라서 SimpleActor에게 어떠한 메시지를 보냈으면 되돌아오는 메시지 역시 보냈던 메시지와 일치해야 한다.

a. expectMsg 사용

"SimpleActor가 보내는 메시지는" should {
	"보냈던 메시지와 일치해야 한다." in {
    	val simpleActor = system.actorOf(Props[SimpleActor])
        val message = "test message"
        
        simpleActor ! message
        
        expectMsg(message)
    }
}

위의 코드를 실행시키면 제대로 결과가 나오는 것을 볼 수 있다.

즉, expectMsg를 통해서 액터가 응답하는 메시지가 올바른지 여부를 판단할 수 있는 것이다.

b. assert 사용

"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 메서드를 사용하여 확인하는 것이다.

(2) BlackholeActor 테스트

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초가 지나도 어떠한 응답도 오지 않아 테스트가 통과된 것을 확인할 수 있다.

(3) LabTestActor 테스트

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이 입력되었을 때 ➡️ 모든 영어 소문자를 대문자로 바꾸어 발송

a. greeting 테스트하기

  "lab test actor는" should {

    "greeting이라는 메시지를 받았을 때 hi나 hello를 응답해야 한다." in {
      val labTestActor = system.actorOf(Props[LabTestActor])
      val MESSAGE = "greeting"

      labTestActor ! MESSAGE

      expectMsgAnyOf("hi", "hello")
    }
  }

expectMsgAnyOf(나올 수 있는 옵션들)이라는 메서드를 사용하면 랜덤으로 응답해도 옵션들 중에 해당 메시지가 있다면 테스트를 통과한다.

b. favoriteTech 테스트하기

1. expectMsgAllOf(모든 경우의 수)

"lab test actor는" should {
	    "favoriteTech라는 메시지를 받았을 때 Scala와 Akka 2개의 메시지를 응답해야 한다." in {
      val labTestActor = system.actorOf(Props[LabTestActor])
      val MESSAGE = "favoriteTech"

      labTestActor ! MESSAGE

      expectMsgAllOf("Akka", "Scala")
    }
}

여러 개의 응답이 오는 경우 expectMsgAllOf(받는 모든 메시지)를 입력해야 한다.

그렇다면 위와 같이 통과가 되는 것을 알 수 있다.

1. 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에 받을 수 있는 모든 응답을 입력하는 것이다.

그렇게 된다면 위와 같이 모두 통과하는 것을 알 수 있다.

0개의 댓글