web/ko/specs.textile (222 lines of code) (raw):

--- prev: coll2.textile next: concurrency.textile title: specs로 테스트 하기 layout: post --- 이번 강좌에서는 Specs를 사용해 테스트하는 법을 다룬다. Specs는 스칼라용 동작 주도 설계(Behavior-Driven Design, BDD) 프레임워크이다. * "Specification(명세 클래스) 확장하기":#example ** 내포 * "실행 모델":#scope * "준비작업과 정리작업-Setup, Teardown":#setup ** doFirst ** doBefore ** doAfter * "매처-Matcher":#matchers ** mustEqual ** contains ** sameSize? ** 필요한 매처 만들어내기 * "목업":#mocks * "스파이":#spies * "sbt에서 실행하기":#sbt h2(#example). Specifcation(명세 클래스) 확장하기 코드부터 살펴보자. <pre> import org.specs._ object ArithmeticSpec extends Specification { "Arithmetic" should { "add two numbers" in { 1 + 1 mustEqual 2 } "add three numbers" in { 1 + 1 + 1 mustEqual 3 } } } </pre> *Arithmetic* 은 *명세를 지정할 시스템(System under Specification)* 이다. *add* 는 컨택스트(context)라 한다. *add two numbers*와 *add three numbers* 는 예제(example)라 한다. @mustEqual@ 은 *예상결과(expectation)* 이다. @1 mustEqual 1@ 은 "예상결과"를 담을 수 있는 틀로 활용할 수 있다. 예제는 반드시 하나 이상의 예상결과를 포함해야 한다. h3. 중복 두 테스트의 이름에 @add@ 가 같이 포함되어 있는게 보이는가? 이런 중복은 예상결과를 *내포* 시킴으로써 없앨 수 있다. <pre> import org.specs._ object ArithmeticSpec extends Specification { "Arithmetic" should { "add" in { "two numbers" in { 1 + 1 mustEqual 2 } "three numbers" in { 1 + 1 + 1 mustEqual 3 } } } } </pre> h2(#scope). 실행 모델 <pre> object ExecSpec extends Specification { "Mutations are isolated" should { var x = 0 "x equals 1 if we set it." in { x = 1 x mustEqual 1 } "x is the default value if we don't change it" in { x mustEqual 0 } } } </pre> h2(#setup). Setup(준비작업)과 Teardown(정리작업) h3. doBefore & doAfter <pre> "my system" should { doBefore { resetTheSystem() /** user-defined reset function */ } "mess up the system" in {...} "and again" in {...} doAfter { cleanThingsUp() } } </pre> *NOTE* @doBefore@ / @doAfter@ 는 말단 예제에 대해에서만 실행된다. h3. doFirst & doLast @doFirst@ / @doLast@는 일회성 설정을 위한 장치이다. (예제가 필요하다. 나(즉, 원저자)는 이를 사용하지 않는다.) <pre> "Foo" should { doFirst { openTheCurtains() } "test stateless methods" in {...} "test other stateless methods" in {...} doLast { closeTheCurtains() } } </pre> h2(#matchers). Matcher(매처) 데이터가 있고, 그 데이터가 올바른지 확인할 때 매처를 사용한다. 가장 흔히 사용되는 것들을 살펴보자. ("매처 가이드":https://code.google.com/p/specs/wiki/MatchersGuide 를 살펴 보라) h3. mustEqual 이미 mustEqual 예제는 여러 개 살펴 보았다. <pre> 1 mustEqual 1 "a" mustEqual "a" </pre> 동일성에는 참조 동일성과 값 동일성이 있다. h3. 열의 원소 <pre> val numbers = List(1, 2, 3) numbers must contain(1) numbers must not contain(4) numbers must containAll(List(1, 2, 3)) numbers must containInOrder(List(1, 2, 3)) List(1, List(2, 3, List(4)), 5) must haveTheSameElementsAs(List(5, List(List(4), 2, 3), 1)) </pre> h3. 맵의 원소 <pre> map must haveKey(k) map must notHaveKey(k) map must haveValue(v) map must notHaveValue(v) </pre> h3. 수 <pre> a must beGreaterThan(b) a must beGreaterThanOrEqualTo(b) a must beLessThan(b) a must beLessThanOrEqualTo(b) a must beCloseTo(b, delta) </pre> h3. 옵션(Option) <pre> a must beNone a must beSome[Type] a must beSomething a must beSome(value) </pre> h3. throwA <pre> a must throwA[WhateverException] </pre> 이렇게 쓰는게 테스트 몸체에서 예외를 캐치해서 실패로 만드는 것 보다 더 짧다. 또한 특정 예외 메시지를 명시할 수도 있다. <pre> a must throwA(WhateverException("message")) </pre> 예외의 타입에 대해 매치할 수도 있다. <pre> a must throwA(new Exception) like { case Exception(m) => m.startsWith("bad") } </pre> h3. 필요한 매처 만들어내기 <pre> import org.specs.matcher.Matcher </pre> h4. val로 정의하기 <pre> "A matcher" should { "be created as a val" in { val beEven = new Matcher[Int] { def apply(n: => Int) = { (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n)) } } 2 must beEven } } </pre> 매처 정의시 지켜야할 계약은 3-튜플을 반환하는 것이다. 첫 원소에는 예상 결과를 만족하는지 여부, 두번째 원소에는 만족시의 메시지, 마지막 원소에는 실패시의 메시지가 들어가야 한다. h4. 케이스 클래스로 정의하기 <pre> case class beEven(b: Int) extends Matcher[Int]() { def apply(n: => Int) = (n % 2 == 0, "%d is even".format(n), "%d is odd".format(n)) } </pre> 케이스 클래스로 만들면 더 공유하기 쉬워진다. h2(#mocks). 목업 <pre> import org.specs.Specification import org.specs.mock.Mockito abstract class Foo[T] { def get(i: Int): T } object MockExampleSpec extends Specification with Mockito { val m = mock[Foo[String]] m.get(0) returns "one" m.get(0) there was one(m).get(0) there was no(m).get(1) } </pre> *See Also* "Mockito 활용하기":https://code.google.com/p/specs/wiki/UsingMockito h2(#spies). 스파이 스파이를 사용해 실제 객체를 "부분적으로 흉내내기"할 수 있다. <pre> val list = new LinkedList[String] val spiedList = spy(list) // methods can be stubbed on a spy spiedList.size returns 100 // other methods can also be used spiedList.add("one") spiedList.add("two") // and verification can happen on a spy there was one(spiedList).add("one") </pre> 하지만, 스파이를 쓰면 때로 문제가 생길 수 있다. <pre> // 리스트가 비어있다면, IndexOutOfBoundsException가 발생할 것이다. spiedList.get(0) returns "one" </pre> 이 경우 @doReturn@ 을 사용해야만 한다. <pre> doReturn("one").when(spiedList).get(0) </pre> h2(#sbt). 개별 specs를 sbt에서 실행하기 <pre> > test-only com.twitter.yourservice.UserSpec </pre> 라고 하면 해당 spec을 실행할 것이다. <pre> > ~ test-only com.twitter.yourservice.UserSpec </pre> 라고 하면 테스트를 실행하되, 파일이 바뀔 때마다 테스트가 재실행될 것이다.