신규 블로그를 만들었습니다!

2020년 이후부터는 아래 블로그에서 활동합니다.

댓글로 질문 주셔도 확인하기 어려울 수 있습니다.

>> https://bluemiv.tistory.com/

Chapter 14. 단언문과 테스트

스칼라에서 단언문과 테스트를 작성하기 위한 여러 방법을 소개

14.1. 단언문

scala 에서는 assert (메소드)를 통해서 단언문(assertion)을 작성.

  • 단언문은 조건을 만족하지 못하면 AssertionError 를 발생

인자 2개를 받는 assertion 도 존재하는데, "조건"과 "설명"을 받는다.

  • 조건: 조건에 만족하지 못하는 경우, AssertionError 을 발생
  • 설명: 설명을 포함하여 AssertionError 를 발생 시킴. Any 타입을 가짐
assert("조건", "설명")

설명은 Any 타입을 가지기 때문에, assert 는 문자열 설명을 얻기 위해 toString 메소드를 호출한다.

def above(that: Element): Element = {
  val this1 = this widen that.width
  val that1 = that widen this.width
  assert(this1.width == that1.width)
  elem(this1.contents ++ that1.contents)
}

위와 같이 검사가 가능한데, 더 간결하게 하고 싶다면 Predef 에 있는 ensuring 이라는 도우미 메소드를 사용할 수 있다.

private def widen(w: Int): Element =
  if (w <= width)
    this
  else {
    val left = elem(' ', (w - width) / 2, height)
    var right = elem(' ', w - width - left.width, height)
    left beside this beside right
  } ensuring (w <= _.width)

ensuring 메소드는 암시적 변환을 사용하기 때문에, 어떠한 결과 타입이든 적용 가능하다.

ensuring 은 인자 하나를 받는데, 그 인자는 술어함수(predicate function)이다.

  • 술어: 메소드 결과의 타입을 받아서 Boolean을 반환하는 함수

술어란?

  • w <= _.width: 술어
  • _(밑줄)은 술어가 갖는 유일한 인자. widen 메소드의 (Element 타입의) 결과

흐름 순서

  1. ensuring 은 술어에게 메소드 결과를 넘긴다.
  2. 술어는 true 또는 false 를 반환한다.
    1. 술어가 true 를 반환하면, ensuring 도 그대로 true 를 반환한다.
    2. 반면, false를 반환하면 ensuringAssertionError 를 발생시킨다.

JVM 에서 -ea 나 -da 명령행 옵션을 이용하여 assertensuring 동작을 켜거나 끌 수 있다.

단위 테스트는 스스로 데이터를 제공하며, 애플리케이션과 독립적으로 실행할 수 있다.

14.2. 스칼라에서 테스트하기

테스트를 할 때, 여러가지 테스트 도구가 존재한다.

  • JUnit, TestNG, ScalaCheck 등등

스칼라 테스트는 가장 유연한 스칼라 프레임워크로서, 쉽게 커스터마이징이 가능하다.

유연하다는 뜻은 팀 (취향)에 맞게 어떠한 테스트 스타일도 사용 할 수 있다는 뜻.

JUnit 에 익숙한 팀원들은 FunSuite 를 이용할 수 있다.

예: FunSuite로 테스트 작성하기

import org.scalatest.FunSuite
import Element.elem
class ElementSuite extends FunSuite {
  test("elem result should have passed width") { 
    val ele = elem('x', 2, 3)
    assert(ele.width == 2)
  }
}

스칼라테스트에서 중요한 개념은 suite 이다.

  • test 는 시작해서 성공 or 실패 or 대기(pending) or 취소와 같이 끝날 수 있다.

트레이트 Suite 는 테스트를 실행하기 전에 사전에 준비된 '생명주기(Life Cycle)' 메소드들을 선언한다.

  • 스타일 트레이트(styletrait): 다른 테스트 스타일을 지원하기 위해 Suite 를 확장하고 생명 주기 메소드를 override 하거나, 믹스인 트레이트(mixin trait)가 가능.

14.3. 충분한 정보를 제공하는 실패 보고

단언문이 실패를 하면, 파일이름, 단언문의 줄 번호, 그리고 정보가 담긴 오류 메시지가 오류보고에 포함된다.

scala> val width = 3
width: Int = 3
scala〉 assert(width == 2)
org.scalatest.exceptions.TestFailedException:
  3 did not equal 2

DiagrammedAssertions

더욱 상세한 정보를 다이어그램 형식으로 보고싶을때, DiagrammedAssertions 을 이용할 수 있다.

scala〉 assert(List (1, 2, 3).contains(4)) org.scalatest.exceptions.TestFailedException:
  assert(List(1, 2, 3).contains(4))
         |    |  |  |  |        |
         |    1  2  3  false    4
         List(123)

assertResult

실제와 기대치가 다르다는 사실을 강조하고 싶다면, assertResult 를 이용한다.

assertResult (2) { // 중괄호 안의 값이 2가 되기를 기대
  ele.width // 만약 코드값이 3인 경우, "Excepted 2, but got 3"
}

assertThrows

만약 예외를 검사하고 싶다면, assertThrows 메소드를 이용한다.

  • 다른 예외를 발생시키거나 예외가 안일어난다면, TestFailedException 이 발생한다.
assertThrows[IllegalArgumentException] {
  elem(,x,, -1, 3)
}
// 도움이 될만한 오류 메시지
// Expected IIlegalArgumentException to be thrown,
//  but NegativeArraySizeException was thrown.

intercept

더 나아가 어떤 예외가 발생했는지 알고 싶을때는 intercept 메소드를 사용한다. assertThrows 와 동일하게 동작하지만, 예상한대로 예외가 발생하는 경우 intercept 만 그 예외를 반환한다.

val caught =
  intercept[ArithmeticException] { 1 / 0 }

assert(caught.getMessage == "/ by zero")

Summary
DiagrammedAssertions: 다이어그램
assertResult: 기대값
assertThrows: (기대하는) 예외
intercept: assertThrows 와 동작은 같지만, 예외 반환

14.4. 명세로 테스트하기

동작 주도 개발 (BDD, Behavior-driven development) 테스트 스타일은 코드의 동작을 사람이 읽을 수 있는 명세로 작성하고, 그 명세에 따라 작동하는 확인하는 방법

FlatSpec

명세 절을(specifier clause) 을 사용해 테스트를 작성

import org.scalatest.FlatSpec
import org.scalatest.Matchers
import Element.elem

class ElementSpec extends FlatSpec with Matchers {
  "A UniformElement" should
      "have a width equal to the passed value" in {
    val ele = elem('x', 2, 3)
    ele.width should be (2)
  }
  it should "have a height equal to the passed value" in {
    val ele = elem('x', 2, 3)
    ele.height should be (3)
  }
  it should "throw an IAE if passed a negative width" in {
    an [IllegalArgumentException] should be thrownBy {
      elem('x', -2, 3)
    }
  }
}
  1. 테스트 할 주제(subject)에 대해 이름을 붙인다.
class ElementSpec extends FlatSpec with Matchers {
  "A UniformElement" ... // 테스트 할 주제
...
  1. 그 뒤에 should(또는 must 또는 can)을 넣는다.
...
class ElementSpec extends FlatSpec with Matchers {
"A UniformElement" should // 주제 다음에 should
...
  1. 그 뒤에 해당 주제의 작동을 설명하고 in이 온다.
...
"A UniformElement" should
  "have a width equal to the passed value" in // 작동 설명
...
  1. in 뒤에는 중괄호안에 테스트할 코드를 넣는다.
... in { // in 다음에
  // 테스트 할 코드들
  val ele = elem('x', 2, 3)
  ele.width should be (2)
}

... in {
  val ele = elem('x', 2, 3)
  ele.height should be (3)
}
...

위와같이 테스트 코드를 작성하고, 실행하면

scala> (new ElementSpec).execute()
A UniformElement
- should have a width equal to the passed value 
- should have a height equal to the passed value
- should throw an IAE if passed a negative width

연결자(matcher) 도메인 특화 언어(DSL, domain-specific language)

Matchers 트레이트를 혼합하면, 자연어처럼 잘 읽을 수 있는 단언문을 작성할 수 있다.

// 예1
should be
// 예2: an[ ... ] should be thrownBy { ... }
...
  an [IllegalArgumentException] should be thrownBy {
    elem('x', -2, 3)
  }

만약, must 를 선호 한다면? MustMatchers 를 혼합하여 사용하면 됨.

result must be >= 0
map must contain key 'c'

// 'c'를 포함하지 않는 경우 결과
Map('a' -> 1, 'b' -〉 2》 did not contain key 'c'

스펙스2(specs2)

스칼라 오픈소스 도구인 스펙스2(specs2)테스트 프레임워크도 BDD 스타일을 지원한다. (문법은 조금 다름)

import org.specs2._
import Element.elem

object ElementSpecification extends Specification {
  "A UniformElement" should {
    "have a width equal to the passed value" in {
      val ele = elem('x', 2, 3)
      ele.width must be_==(2)
    }
    "have a height equal to the passed value" in {
      val ele = elem('x', 2, 3)
      ele.height must be_==(3)
    }
    "throw an IAE if passed a negative width" in {
      elem('x', -2, 3) must throwA[IllegalArgumentException]
    }
  }
}

이러한 테스트들은...

이러한 테스트들의 장점은 사람 사이의 의사소통을 테스트가 도와줄 수 있다는 점이다.

  • 스칼라테스트, 스펙스2 둘다 가능하지만, 스칼라테스트의 FeatureSpec은 이런 목적으로 설계된 것이다.
import org.scalatest._

class TVSetSpec extends FeatureSpec with GivenWhenThen {
  feature("TV power button") { // 전원 버튼
    scenario ("User presses power button when TV is off") {
      // 사용자가 TV 전원을 끄는 버튼을 눌렀다.
      // (그렇다면 구체적으로 어떻게?)
      Given ("a TV set that is switched off") // TV의 전원 스위치를 off
      When("the power button is pressed") // 전원 버튼을 눌렀을 때,
      Then("the TV should switch on") // 단, TV 전원 스위치가 on 이어야 함
      pending // 대기
    }
  }
}

참고로 GivenWhenThen 트레이트가 Given, When, Then 을 제공함.

14.5. 프로퍼티 기반 테스트

스칼라체크 ScalaCheck는 스칼라로 만들어진 또 다른 유용한 테스트 도구다.

  • 테스트할 코드가 준수해야 하는 프로퍼티를 명시.
  • 각 프로퍼티에 대해 테스트 데이터를 생성한 다음, 프로퍼티를 잘 지키는지 검사하는 테스트를 실행.

PropertyChecks 트레이트와 WordSpec

import org.scalatest.WordSpec
import org.scalatest.prop.PropertyChecks
import org.scalatest.MustMatchers
import Element.elem

class ElementSpec extends WordSpec with PropertyChecks {
  "elem result" must {
    "have passed width" in {
      forAll {
        (w: Int) => whenever (w > 0) {
          elem('x', w, 3).width must equal (w)
        }
      }
    }
  }
}

스칼라 체크의 테스트

스칼라체크는 프로퍼티에 맞지 않는 값을 찾기 위해, w 에 들어갈 수 있는 값을 수백 개 생성하고 테스트 한다.

  • 스칼라체크가 시도하는 모든 값을 프로퍼티가 만족하는 경우, 테스트를 통과.
  • 만족하지 않으면, TestFailedException 을 즉시 던지면서 테스트 종료.

14.6. 테스트 조직과 실행

스칼라테스트에서는 스위트(suite) 안에 스위트를 포함시킴으로써 큰 테스트를 조직화한다.

어떤 Suite가 실행되면, 그 안에 있는 테스트, 내부에 있는 Suite의 테스트도 실행된다.

즉, 트리구조의 루트 Suite 객체를 실행하면 트리 전체의 Suite 를 실행하게된다.

수동 처리

만약, 수동으로 처리하려면 nestedSuites 메소드를 오버라이드하거나, 포함시키고 싶은 스위트를 Suite 클래스의 생성자에 전달한다.

자동 처리

스칼라테스트의 Runner 에 패키지 이름을 전달 하면 된다.

실행자(Runner)가 자동으로 스위트를 찾아내서 루트 스위트 안에 그 스위트들을 넣고 루트 스위트를 실행한다.

전반적인 내용을 알기 위해서는 각 프레임워크 문서를 확인할 필요가 있다.

빌드툴을 이용한 Runner 실행

명령행에서 실행하거나 sbt,maven, ant 같은 빌드 툴을 통해 스칼라테스트의 Runner 애플리케이션을 호출할

  • 명령행에서 가장 Runner 를 쉽게 호출하는 방법은 org.scalatest.run 어플리케이션을 이용하는 방법
# 컴파일
$ scalac -cp scalatest.jar TVSetSpec.scala

 

# 실행
$ scala -cp scalatest.jar org.scalatest.run TVSetSpec
  • -cp scalatest.jar: -cp 옵션을 통해 JVM 의 클래스 경로에 스칼라테스트 .jar 파일을 추가
  • org.scalatest.run: 애플리케이션의 전체 경로
  • TVSetSpec: 실행할 스위트

'Language > Scala' 카테고리의 다른 글

Scala 기본 타입과 연산 (Chapter 05)  (0) 2019.11.10
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기