4장 테스팅 도구

들어가며....

모든 예제는 avtivator 에서 sample 로 제공 하는 test-patterns-scala 프로젝트 를 기준으로 작성 되었으며 책의 내용과 맥락 을 같이합니다.

DSL 란?

스칼라의 테스팅 기법에서 DSL(Domain Specific Language) 이란 도메인 명세 언어로서 도메인을 통하여 모두가 이해할수 있는 언어 입니다 DSL 의 3대 요소에는 단순함,간결함,유창함 있으며 개발자나 클라이언트 DSL 로 작성된 스크립트를 보고 의미를 쉽게 파락해야 하고(단숨함) 간단하면서 표현력 있는 문법을 사용해야 합니다(간결함) 그리고 이해할수 있는 문맥이 존재한다면 설명이 없이도 의미를 빠르게 파악할수 있습니다(유창함)

BDD 란?

  • Dan North 의 BDD(Behaviour-Driven Development) 행도 주도 개발 은 도메인의 요구 사항에 집중한 테스팅 기법으로 기존 TDD 테스트 주도 개발 과 다른 점은 기능에 초점을 준 단위 테스트 기법 인지, 행동에 집중한 도메인 관점 테스트 인지 의 차이 입니다.

  • BDD 는 문장 템플릿 기반 으로 서술에 집중한 테스트 기법 으로 책 뒷 부분에 예제가 나옵니다 여기서 는 어떤 자연어 규칙으로 테스트 기법을 작성 하는지에 대한 포인트만 작성 하겠습니다.

[x] 로 하여금 (AS a)
[Z] 를 얻게 하기 위하여 (so that)
[Y] 가 필요하다 (I want)

Given 조건 이 주어짐.
When 만일 사건이 발생 했을때.
Then 그러면 어떤 일이 수행된다.

에릭 에반스 의 DDD 에서는 도메인 전문가 와 개발자의 공통언어(ubiquitous language) 에 대하여 설명 합니다 개념은 비개발자와 개발자 간의 모델 기반 언어를 통해 설계 해야 그 코드가 시스템에 잘 녹아 들어간다는 것 입니다 Dan North 는 이러한 사상을 바탕으로 공통 언어 템플릿을 정의 하였습니다.

스칼라 테스트 시작

activator 를 설치한 디렉토리로 이동 커맨드 창에서 아래 내용 입력

> activator ui
  • test-patterns-scala 프로젝트 검색후 생성 -> 왼쪽 Code 클릭 -> Browse Code 의 ... 클릭 -> create IntelliJ project 생성
  • IntelliJ 실행후 import project 통해 생성된 프로젝트를 로드한다

ScalaTest

테스트 예제는 샘플에 모두 구현되어 있으므로 중요한 요점만 표시

  • 테스트 구동
    • > activator
    • > test-only 실행 클래스 이름 (이름 예: scalatest.Test01.scala)
  • 테스트 연속 모드
    • 테스트 연속 모드는 저장될 때마다 SBT 에서 테스트를 자동으로 구동 합니다
    • > ~test-only scalatest.test02

Example 1

package scalatest

import org.scalatest.FunSuite
// == 비교문에 = 한번더 추가시 에러내용을 표기해 준다
class Test02 extends FunSuite {
  test("pass") {
        assert("abc" === "abc")
    }
    test("fail and show diff") {
        assert("abc" === "abcd") // 에러 발생 및 내용 표시
    }
}

Example 2

ScalaTest 를 JUnit 테스트로 실행 하기 위하여 설정이 필요하다

  • build.sbt
libraryDependencies ++= Seq(
...
...
"junit" % "junit" % "4.11" % "test", // 의존성 추가
...
)
  • 테스트 작성시 @RunWith(classOf[JUnitRunner]) 추가해 준다
package scalatest

import org.junit.runner.RunWith
import org.scalatest.{BeforeAndAfter, FunSuite}
import org.scalatest.junit.JUnitRunner

@RunWith(classOf[JUnitRunner])
class MyTestSuite extends FunSuite with BeforeAndAfter {

  var sb : StringBuilder = _

  before{
    sb = new StringBuilder("scala is")
  }

  test("테스트 시작 해요"){
    sb.append("easy !")
    //("ScalaTest is easy!" === sb.toString())
    assert(!sb.isEmpty)
    println("ddd")
   }

}
  • 클래스 선택후 -> 마우스 우클릭 -> Run -> 녹색 막대 확인

Example 3

BDD 스타일 테스팅

package scalatest

import org.scalatest.FeatureSpec
import org.scalatest.GivenWhenThen
import scala.collection.mutable.Stack

//Taken from http://www.scalatest.org/getting_started_with_feature_spec
//Several test styles supported by ScalaTest
//GivenWhenThen 은 deprecate 상태로 앞 글자가 대문자로 변경 되었으며 내부적으로도 변경된 함수를 호출 하고 있다
class Test06 extends FeatureSpec with GivenWhenThen {

  feature("The user can pop an element off the top of the stack") {
 //사용자는 스택의 선두로부터 요소를 팝 할 수 있습니다.
        info("As a programmer")
        //프로그래머로 하여금
        info("I want to be able to pop items off the stack")
        //나는 스택에서 항목을 팝 할수있게 원합니다
        info("So that I can get them in last-in-first-out order")
    //나는 후입 선출 순서 로 그것을 얻게 하기 위하여
        scenario("pop is invoked on a non-empty stack") {
      // 팝 은 비어 있지 않는 스택에 저장됩니다
          given("a non-empty stack")
                //빈 스택
          val stack = new Stack[Int]
          stack.push(1)
          stack.push(2)
          val oldSize = stack.size

          when("when pop is invoked on the stack")
                //스택 에서 팝을 호출 할 때
          val result = stack.pop()

          then("the most recently pushed element should be returned")
                //가장 최근에 푸시 요소를 반환해야합니다
          assert(result === 2)

          and("the stack should have one less item than before")
                //스택은 이전보다 1 적은 아이템을 가지고 있어야합니다
          assert(stack.size === oldSize - 1)
        }

        scenario("pop is invoked on an empty stack") {
                //빈 스택에서 팝을 호출 합니다
          given("an empty stack")
          val emptyStack = new Stack[Int]

          when("when pop is invoked on the stack")
                //스택 에서 팝을 호출 할때
          then("NoSuchElementException should be thrown")
                //NoSuchElementException 이 발생 되어야 합니다
          intercept[NoSuchElementException] {
            emptyStack.pop()
          }

          and("the stack should still be empty")
                //스택이 비어 있어야 합니다
          assert(emptyStack.isEmpty)
        }
      }
    }

BDD 테스팅은 직관적 인 테스트 로서 테스트 자체가 문서화 가능하며 도입 부분에 설명했던 공통언어 가 잘 드러나 있습니다

Example 4

일반 문장에 가까운 형태로 assertion 테스트

package scalatest

import org.scalatest._
import org.scalatest.matchers.ShouldMatchers
//Matchers 로 변경됨
//class Test07 extends FlatSpec with ShouldMatchers {
class Test07 extends FlatSpec with Matchers {
  "This test" should "pass" in {
    true should be === true
  }
}

Matchers 자세한 내용은 아래 링크를 참조하세요! http://www.scalatest.org/user_guide/using_matchers

Example 5

스칼라 워크시트 추가

  • 인텔리J -> new -> Scala Worksheet Worksheet 를 활용 하면 테스트 실패시 분활된 화면으로 즉시 메시지를 표시해 줍니다
package scalatest

import org.scalatest._

object ShouldWork extends FlatSpec with Matchers {

  true should be === false
}

Worksheet 테스트시 워크시트 평과 과정에서 실패가 도출되면 즉시 멈추며 문제가 생긴 부분을 해결해야만 다음 테스트가 가능합니다

Example 6

Selenium 테스팅 도구 (http://www.seleniumhq.org)

ackage scalatest

import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.selenium.WebBrowser
import org.openqa.selenium.htmlunit.HtmlUnitDriver
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.WebDriver

class Test08 extends FlatSpec with Matchers with WebBrowser {
  // 드라이버 변경으로 여러 환경 테스트 가능 (파폭,크롬,사파리 등등)
  implicit val webDriver: WebDriver = new HtmlUnitDriver

    go to "http://www.amazon.com"
    click on "twotabsearchtextbox"
    textField("twotabsearchtextbox").value = "Scala"
    submit()
    pageTitle should be ("Amazon.com: Scala")
    pageSource should include("Scala Cookbook: Recipes")
}
  • Selenium 의 UI 테스트는 초기 학습 비용이 들고 테스트 작성이 까다롭다 그리고 테스트 시나리오 작성이 모호합니다
  • Selenium 테스트 대해 모두 생각해 봤으면 좋겠습니다...
  • 자세한 내용은 selenium 테스트 검색 키워드로 많은 정보를 얻을수 있습니다

Example 6

ScalaMock 모킹하기

  • 스칼라 에서는 트레잇과 같은 자바에는 없는 요소가 있어 자바 기반의 모킹 프레임워크로는 부족하여 scalaMock(www.scalamock.org) 이 등장하게 되었 습니다 *
  • mock 객체 는 행위를 검증하기 위한 가짜 객체를 말합니다 mock 테스트 를 말할때 빠질수 없는 개념인 테스트 더블(test double) 대역을 나타내는 스턴트 더블에서 차용된 말로 오리지널 객체로 테스트가 힘들경우 이를 대신해 준다는 개념 입니다

start

  • 의존성 추가 (참고: scalaMock 공식 페이지 접속시 2014년 부로(scalaMock3) 더이상 업데이트가 없으며 2016년에 scalaMock4 로 업데이트 예정)
"org.scalamock" %% "scalamock-scalatest-support" % "3.2.2" % "test",

Example 7

ScalaCheck(https://www.scalacheck.org)

테스트 환경이 완전한 형태를 갖추기 힘든 경우 또는 테트스 범위가 너무 커서 테스트 데이터를 만드기 힘든 경우 scalaCheck 는 자동화된 속성기반 테스트 를 만들수 있다

//build.sbc 의존성 주입
resolvers += Resolver.sonatypeRepo("releases")

"org.scalamock" %% "scalamock-scalatest-support" % "3.0.1" % "test",

//test 코드 작성
package se.chap4

import org.scalacheck.Properties
import org.scalacheck.Prop.forAll

object StringSpecification extends Properties("String") {
//Properties 큰 묶음표현

  property("startsWith") = forAll{ (a: String, b: String) =>
  //property 단위 표현
    (a+b).startsWith(a)
  }

  property("concatenate") = forAll { (a: String, b: String) =>
    (a+b).length > a.length && (a+b).length > b.length
  }

  property("substring") = forAll { (a: String, b: String, c: String) =>
    (a+b+c).substring(a.length, a.length+b.length) == b
  }

}

//결과
+ String.startsWith: OK, passed 100 tests.
! String.concatenate: Falsified after 0 passed tests.
> ARG_0: ""
> ARG_1: ""
+ String.substring: OK, passed 100 tests.

Process finished with exit code 1

scalaCheck 는 테스트 데이터 값을 무작위로 생성하여 속성이 만족하는지에 대한 테스트를 한다

그외 많은 예제를 확인하려면 (http://booksites.artima.com/scalacheck/examples/index.html)

Example 8

package se.chap4

import org.scalacheck._
import Arbitrary._
import Prop.forAll


object StringSpecification extends Properties("String") with Currency{

  //첫번째 테스트 하나의 임의 키값 생성
  val currencies = Gen.oneOf("EUR","GBP","SEK","JPY","error")

  //두전째 해당 리스트 만큼
  lazy val conversions : Gen[(BigDecimal,String,String)] = for {
    //정수인지 양수인지 필터링
    amt <- arbitrary[Int] suchThat {_ >= 0}
    //임의값 맵핑
    from <- currencies
    //임의값 맵핑
    to <- currencies

  }yield(amt,from,to)


  //첫번째 테스트
  property("정의...") = forAll (currencies) { c:String =>
      val amount = BigDecimal(200)
      //계산된 정보
      val convertedAmount = convert(amount,c,c)
      //비교
      convertedAmount == amount
  }
  //두번째 테스트
  property("정의...") = forAll(conversions) { c =>
    //튜플 값
    val convertedAmount = convert(c._1,c._2,c._3)
    //비교
    convertedAmount >= 0
  }

}

그외....

java -> scala 변환기