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>
라고 하면 테스트를 실행하되, 파일이 바뀔 때마다 테스트가 재실행될 것이다.