관리 메뉴

HAMA 블로그

specs2 로 하는 유닛테스트 (번역) 본문

PlayFramework2

specs2 로 하는 유닛테스트 (번역)

[하마] 이승현 (wowlsh93@gmail.com) 2016. 9. 2. 16:58

specs2 로 어플리케이션 테스트하기

어플리케이션 위한  테스트를 작성하는것은 개발 프로세스에 포함 될 수 있다. 플레이는 테스트 작성을 가능한 쉽게 할 수 있도록  기본 테스트 프레임워크를   제공한다. 

살펴보기

테스트를 위한 파일의 위치는 "test" 폴더이다. 거기엔 2개의 샘플 테스트파일이 이미 존재하는데 당신 자신의 테스트를 작성하기 위한 템플릿으로 사용될 수 있을 것 이다. 

플레이 콘솔에서 테스트를 실행 할 수 있다. (IntelliJ 같은 툴에서 실행도 가능) 

  • * 모든 테스트를 실행하기 위해 test 을 run 하라.
  • * 하나의 테스트 클래스를 실행하기 위해서는 run test-only  를 하고 이어서 클래스이름을 써라.
      다음과 같이  test-only my.namespace.MySpec.
  • * 오직 실패한 테스트를 돌리기 위해서 test-quick.
  • * 테스트를 이어서 계속하기 위해  ~test-quick.
  • *FakeRequest  같은 테스트 헬퍼에 접근하기위해 run test:console.

SBT 기반의 플레이를 테스트 하는것과 관련된 전체 문서는 testing SBT 를 참고 하시라

specs2 사용하기

플레이 specs2 를 사용하기 위해 의존성을 다음과 같이 추가하라.

libraryDependencies += specs2 % Test

specs2 는 테스트들이 specifications 로 구성되어 있는데 이것은 다양한 코드패스들을 통해 테스트기반한 시스템을 실행하는 여러가지 예제를 포함한다.

Specifications 는 Specification trait 를 상속하였으며 should/in format 을 사용한다. 

import org.specs2.mutable._

class HelloWorldSpec extends Specification {

  "The 'Hello world' string" should {
    "contain 11 characters" in {
      "Hello world" must have size(11)
    }
    "start with 'Hello'" in {
      "Hello world" must startWith("Hello")
    }
    "end with 'world'" in {
      "Hello world" must endWith("world")
    }
  }
}

Specifications 는 IntelliJ IDEA (using the Scala plugin) 또는 Eclipse (using the Scala IDE) 에서도사용할 수 있다. 자세한건 다음링크를 참조하라.IDE page .

Note:  presentation compiler 버그때문에 이클립스에서테스트들은 반드시 specific format 을 정의해야한다.

  • 패키지는 반드시 디렉토리 패스와 동일해야한다. 
  • specification 반드시 @RunWith(classOf[JUnitRunner]) 주석과 같이 사용되야한다.

여기 이클립스를 위한 올바른 specification 을 보자:

package models // this file must be in a directory called "models"

import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
  ...
}

Matchers 

 반드시 example 결과를 리턴해야한다. 보통  must을 포함한 문장을 보게 될것이다.

"Hello world" must endWith("world")

must 키워드를 가진 평가식은 matche 로 알려져있다. Matchers 는 example result 를 리턴하며, 성공 혹은 실패이다. 만약 result 를 리턴하지 않으면 컴파일하지 않을것이다.

가장 유용한 매쳐는 match results이다.  동등성 체크하는데 사용하며  Option 과 Either 그리고 예외가 발생했는지를 체크하기위해 result 를 확인한다. 

optional matchers 도 있으며 XML 와 JSON matching 도 수행한다.

Mockito

Mocks 은 유닛테스트를 외부 디펜던시와 분리시키기 위해 사용된다. 예를들어 클래스가 외부 데이터 클래스에 의존적이라고 할때  완전한 데이타서비스 객체를 만드는것을 대신해서 적절한 가짜 데이터를 사용 할 수 있게 한다.

Mockito 는 specs2 에 디폴트 mocking library로 추가되어있다.

Mockito를 사용하기위해 다음 import 를 추가하자.

import org.specs2.mock._

다음과 같이 목을 만들 수 있다.

trait DataService {
  def findData: Data
}

case class Data(retrievalDate: java.util.Date)
import org.specs2.mock._
import org.specs2.mutable._

import java.util._

class ExampleMockitoSpec extends Specification with Mockito {

  "MyService#isDailyData" should {
    "return true if the data is from today" in {
      val mockDataService = mock[DataService]
      mockDataService.findData returns Data(retrievalDate = new java.util.Date())

      val myService = new MyService() {
        override def dataService = mockDataService
      }

      val actual = myService.isDailyData
      actual must equalTo(true)
    }
  }
  
}

Mocking 은 특별히 클래스의 공개 메소드를 테스팅하는데 유용하다. Mocking objects 와  private methods 도 가능하지만 좀 어렵다.

모델 유닛 테스팅  

플레이는 특별한 데이타베이스 데이터 접근 레이어를 사용하기 위한 모델을 요구하진 않는데 , 만약 어플리케이션이 Anomr이나 Slick 을 사용한다면, 종종 모델은 내부적으로 데이타접근을 위한 참조를 가질 것이다.

import anorm._
import anorm.SqlParser._

case class User(id: String, name: String, email: String) {
   def roles = DB.withConnection { implicit connection =>
      ...
    }
}

유닛테스팅을 위해 이 경우는  roles 메소드를 목으로 만들 수 있다.

일반적인 접근법은 데이타베이스로부터 모델들을 고립시켜서  로직을 테스트하는것이다.

case class Role(name:String)

case class User(id: String, name: String, email:String)
trait UserRepository {
  def roles(user:User) : Set[Role]
}
class AnormUserRepository extends UserRepository {
  import anorm._
  import anorm.SqlParser._

  def roles(user:User) : Set[Role] = {
    ...
  }
}

그리고 서비스를 통해 그들 (목) 에 접근한다.

class UserService(userRepository : UserRepository) {

  def isAdmin(user:User) : Boolean = {
    userRepository.roles(user).contains(Role("ADMIN"))
  }
}

이런 방식으로 isAdmin 메소드는 Mock(목) UserRepository  를 주입받고 테스팅 된다

object UserServiceSpec extends Specification with Mockito {

  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock[UserRepository]
      userRepository.roles(any[User]) returns Set(Role("ADMIN"))

      val userService = new UserService(userRepository)
      val actual = userService.isAdmin(User("11", "Steve", "user@example.org"))
      actual must beTrue
    }
  }
}

컨트롤러 유닛 테스트 

컨트롤러들이 정규 클래스라면 쉽게 플레이 테스트 헬퍼를 이용하여 유닛테스트를 할 수 있다. 하지만 만약 컨트롤러들이 또다른 클래들에 의존적이라면 dependency injection 를 사용할 수 있다. 이것은 그 의존성을 목(Mock) 으로 만들어 줄 것이다. 

class ExampleController extends Controller {
  def index() = Action {
    Ok("ok")
  }
}

요렇게 테스트 할 수 있다

import play.api.mvc._
import play.api.test._
import scala.concurrent.Future

object ExampleControllerSpec extends PlaySpecification with Results {

  "Example Page#index" should {
    "should be valid" in {
      val controller = new ExampleController()
      val result: Future[Result] = controller.index().apply(FakeRequest())
      val bodyText: String = contentAsString(result)
      bodyText must be equalTo "ok"
    }
  }
}

EssentialAction 유닛테스트 

 Action 과  Filter 를 테스팅 하기 위하여 EssentialAction 이 활용 된다.

Helpers.call 다음과 같이 사용된다.

object ExampleEssentialActionSpec extends PlaySpecification {

  "An essential action" should {
    "can parse a JSON body" in new WithApplication() {
      val action: EssentialAction = Action { request =>
        val value = (request.body.asJson.get \ "field").as[String]
        Ok(value)
      }

      val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))

      val result = call(action, request)

      status(result) mustEqual OK
      contentAsString(result) mustEqual "value"
    }
  }
}

https://www.playframework.com/documentation/2.5.x/ScalaTestingWithSpecs2 번역

Comments