단언문과 테스트는 작성한 소프트웨어가 제대로 동작하는 지 확인할 수 있는 방법

 

 

14.1 단언문

: assert 메소드를 호출하는 방식으로 단언문 작성

: "assert(조건)" - 조건을 만족하지 않을 경우 AssertError 발생

: "assert(조건, 설명)" - 조건을 만족하지 않을 경우 설명을 포함한 AssertError 발생 

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)

}

 

< 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 인자는 술어 함수를 받는다.

: 술어 함수가 True를 반환하면 메소드 결과를 그대로 반환하고 False를 반환하면 AssertionError 발생

: "_"는 술어가 갖는 유일한 인자, 메소드가 반환해야 하는 결과가 들어간다.

: widen의 결과(Element)에 ensuring을 호출하고 있는 것 같지만 Element를 암시적으로 변환한 타입에 대해 ensuring 호출 = 암시적 변환을 사용하기 때문에 어떤 결과 타입이든 적용 가능

 

: JVM에 -ea나 -da 명령행 옵션을 사용하면 assert, ensuring 동작을 켜거나 끌 수 있다.

 

 

14.2 스칼라에서 테스트하기

: 스칼라는 스칼라테스트, 스펙스2, 스칼라체크 테스트 도구 존재

 

< 스칼라 테스트 >

: 가장 유연한 스칼라 테스트 프레임워크 => 테스트 관련 트레이트/클래스를 통해 쉽게 커스터마이징이 가능해 팀의 요구에 맞는 테스트 스타일 사용 가능

- 트레이트 Suite : 테스트들을 실행하기 위해 사전에 준비된 '생명 주기' 메소드가 선언되어 있음

- 스타일 트레이트 : 다른 테스트 스타일을 지원하기 위해 트레이트 Suite를 확장해 생명 주기 메소드 오버라이드

- 믹스인 트레이트 : 특별한 테스트 요구를 해결하기 위해 생명 주기 메소드 오버라이드

=> 스타일 트레이트와 믹스인 트레이트를 통해 테스트 클래스를 정의해 테스트 스위트(테스트 집합)를 만든다.

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)

    }

}

: FunSuite 스타일 트레이트는 test 메소드 안에 테스트 코드 작성

: test는 이름에 의한 호출 파라미터로 주 생성자에서 테스트 코드를 나중에 실행하기 위해 등록

: 인터프리터에서 단순히 execute를 호출함으로써 테스트 실행 

scala> (new ElementSuite).execute()

ElementSuite:

  - elem result should have passed width

 

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

< 기본 내장 assert >

   val a = 3

   assert(a==2) 

< scalatest assert >

   val a = 3

   assert(a==2)

: 스칼라 테스트는 컴파일 시점에 각 assert 호출에 전달된 식 분석 => 상세한 정보 및 서술적인 오류 메시지 출력

: 실제와 기대치가 다르다는 사실을 강조하고 싶다면 assertResult 사용

   val width = 3

   assertResult(2) { width }

: 어떤 메소드가 발생시킬 수 있는 예외를 검사하고 싶다면 assertThrows 사용

assertThrows[ArithmeticException] {

   throw new AbstractMethodError

}

: assertThrows 는 정상적인 예외에 대해 Succeeded라는 단순한 결과를 반환하지만 intercept는 예외 자체를 반환

 

 

14.4 명세로 테스트하기

- 동작 주도 개발( BDD - behavior-driven development ) 테스트 스타일 : 기대하는 코드의 동작을 사람이 읽을 수 있는 명세로 작성하고 코드가 그 명세에 따라 작동하는지 확인하는 테스트를 작성하는 데 중점(요구사항[스펙, 명세]이 테스트 코드가 되고 이 테스트 코드를 기반으로 기능 구현)

import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers
import Element.elem

class ElementSpec extends FlatSpec with ShouldMatchers {

  "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 "throw an IAE if passed a negative width" in {
    an [IllegalArgumentException] should be thrownBy {
      elem('x', -2, 3)
    }  
  }
}

: FlatSpec 스타일 트레이트를 사용하면 BDD 테스트 스타일로 작성 가능

: 형식 => 테스트 주제 should(must, can) 테스트 설명 in { 테스트 코드 }

: it 은 가장 최근에 언급된 테스트 주제 의미 => it은 "A UniformElement"

: "should be", "in", "should be thrownBy" 문법은 ShouldMatchers(연결자 도메인 특화 언어) 믹스인 트레이트에서 제공

              => should 보다 must를 선호한다면 MustMatchers를 믹스인 

              => 연결자 DSL을 통해 커스텀하게 단언문 작성 가능

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

: 실행 시 사람이 읽기 더 좋은 출력을 만든다.

 

 

- BDD는 소프트웨어 시스템을 만들지 결정하는 사람, 그 소프트웨어를 구현하는 사람, 소프트웨어가 잘 마무리되어 동작하는 지 결정하는 사람 사이의 의사소통을 테스트가 도와주도록 해야 한다. 

import org.scalatest._

class TVSetSpec extends FeatureSpec with GivenWhenThen {
  feature("TV power button"){
    scenario("User presses power button when TV is off"){
      Given("a TV set that is switched off")
      When("the power button is pressed")
      Then("the TV sould switch on")
      pending
    }
  }
}

 : FeatureSpec 스타일 트레이트는 의사소통을 돕기 위해 설계

: 구체적인 특징(feature)을 밝혀야 하고 그에 대한 시나리오(scenario)를 명시한다.

: Given, When, Then 은 구체적인 개별 시나리오에 대한 대화에 초점을 맞춘다.

: pending 호출은 테스트나 실제 동작이 아직 구현되지 않았다는 사실 명시

 

 

14.5 프로퍼티 기반 테스트

: 스칼라 체크는 각 프로퍼티에 대해 테스트 데이터를 생성한 다음, 프로퍼티를 잘 지키는지 검사하는 테스트를 실행

import org.scalatest.WordSpec
import org.scalatest.prop.PropertyChecks

import org.scalatest.MustMatcers._
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)
        }
      }
    }
  }
}

: elem 요소가 지켜야 하는 한 가지 프로퍼티 검사

: whenever은 왼쪽 편에 있는 절이 true일 때마다 오른쪽에 있는 식이 true가 되어야함을 명시한다.

: forAll 내부에 테스트 데이터를 인자로 받고 단언문을 수행하는 함수값 명시 => 스칼라 체크가 테스트 데이터를 임의로 수 백개 생성 => 모든 값을 만족하는 경우 테스트를 통과 그렇지 않는 경우 실패 원인이 된 값이 들어있는 TestFailedException을 던지고 종료

 

 

14.6 테스트 조직과 실행

: 스칼라 테스트는 스위트 안에 스위트를 포함시킴으로써 큰 테스트를 조직 => 트리 구조를 형성해 루트 Suite 객체를 실행하면 트리 전체의 Suite를 실행한다.

: 수동 또는 자동으로 스위트 포함 가능

   - 수동 : nestedSuites 메소드 오버라이드 또는 생성자에 Suite 전달

 import org.scalatest.Suite

 class ASuite extends Suite
 class BSuite extends Suite
 class CSuite extends Suite

 class AlphabetSuite extends SuperSuite(
   List(
     new ASuite,
     new BSuite,
     new CSuite
   )
 )

 

 scala> (new AlphabetSuite).run()
 

   - 자동 : 스칼라 테스트의 Runner에 패키지 이름 전달 

$ scalac -cp scalatest.jar TVSetSpec.scala

$ scala -cp scalatest.jar org.scalatest.run TVSetSpec

 

- 프로그램의 여러 부분이 서로 의존하는 정도를 나타내는 커플링(Coupling)을 최소화하는 것이 중요 

       => 모듈화 스타일로 프로그램을 작성(프로그램을 더 작은 여러 모듈로 나눈다)

- 모듈 내부를 작업할 때는 같은 모듈을 가지고 작업하는 프로그래머와 협력

- 모듈 외부를 변경해야 하는 경우에만 다른 모듈을 가지고 작업하는 프로그래머와 협력

 

 

13.1 패키지 안에 코드 작성하기

1. package 절을 사용해 파일 전체를 패키지 안에 넣는다.

package bobsrockets.navigation

class Navigator

2. package 절 다음, 중괄호 안에 있는 정의를 모두 패키지에 넣는다.(패키징) => 한 파일 안에 여러 패키지

package bobsrockets {
   package navigation {    

     class Navigator
     package tests {
           class NavigatorSuite
      }
   }
}

 

 

13.2 관련 코드에 간결하게 접근하기

package bobsrockets {
   package navigation {
      class Navigator {
           val map = new StarMap     // 1 
       }
      class StarMap
   }
   class Ship {
          val nav = new navigation.Navigator       // 2
    }
    package fleets {
         class Fleet {    
           def addShip() = { new Ship }    // 3
         }
    }
}

1. 어떤 클래스가 속한 패키지 안에서는 접두사가 없어도 해당 클래스에 접근할 수 있다.

2. 어떤 패키지를 포함하는 패키지 안에서는 해당 패키지에 어떤 접두어도 붙이지 않고 접근할 수 있다.

3. 패키지 밖에서 접근 가능한 모든 이름을 그 패키지 안에서도 쓸 수 있다.

 

중괄호를 사용하기 싫다면 아래와 같이 중첩 패키지 구현 가능(연쇄 패키지 절)

package bobsrockets

package fleets

class Fleet{

     de addShip() = { new Ship }

}

 

Ex. 모든 최상위 패키지는 _root_ 패키지의 멤버로 취급

// In file lanch.scala
package launch {
class Booster3
}
// In file bobsrockets.scala
package bobsrockets {
  package navigation {
    package launch {
      class Booster1
    }
    class MissionControl {
      val booster1 = new launch.Booster1
      val booster2 = new bobsrockets.launch.Booster2
      val booster3 = new _root_.launch.Booster3
    }
  }
  package launch {
    class Booster2
  }
}

 

13.3 임포트

: 다른 패키지의 멤버에 접근할 때 전체 경로를 명시하지 않고 간단한 이름으로 접근하게 해준다.

 

Ex. 

package bobsdelights

abstract class Fruit(
  val name: String,
  val color: String
)

object Fruits {
  object Apple extends Fruit("apple", "red")
  object Orange extends Fruit("orange", "orange")
  object Pear extends Fruit("pear", "yellowish")
  val menu = List(Apple, Orange, Pear)
}

- import bobsdelights.Fruit  => 자바의 싱글 타입 임포트와 같다. Fruit에 간단하게 접근

- import bobsdelights._  => 자바의 import bobsdelights.* 와 같다. bobsdelights의 모든 멤버에 간단하게 접근

- import bobsdelights.Fruits._ => Fruits의 모든 멤버에 간단하게 접근

 

< 자바와 다른 스칼라의 유연한 import >

- 스칼라의 임포트는 코드의 어디에라도 들어갈 수 있다. 

- 임의의 값, 객체를 임포트할 수 있다.

def showFruit(fruit: Fruit) = {
   import fruit._
   println(name + "s are " + color)
}

 

- 패키지 자체도 임포트 가능

import java.util.regex      // 자바는 불가능하다. 자바는 java.util.regex.* 또는 java.util.regex.Pattern  
class AStarB {
   val pat = regex.Pattern.compile("a*b")
}

 

- 임포트 셀렉터를 통해 불러온 멤버 이름을 숨기거나 다른 이름을 지정할 수 있다.

import Fruits.{Apple, Orange}   // Fruits 객체에 있는 Apple과 Orange만 불러온다.

 

import Fruits.{Apple=>McIn, Orange}       // Apple 객체 이름을 McIn로 바꾼다. => Apple 객체는 McIn로 참조 

 

import Fruits.{Apple=>McIn, _}     // Fruits의 모든 멤버를 불러오나 Apple의 이름을 McIn으로 바꾼다.

 

import Fruits.{Pear => _ , _}     // Fruits에서 Pear를 제외한 모든 멤버를 불러온다.

 

13.4 암시적 임포트

스칼라는 모든 프로그램에 아래 임포트를 암묵적으로 추가한다.

import java.lang._     => java.lang.Thread 대신에 Thread 

import scala._          => 많이 사용하는 클래스 및 객체와 표준 스칼라 라이브러리, scala.List 대신에 List

import Predef._        => 암시적 변환 포함, Predef.assert 대신 assert

- 스칼라는 나중에 임포트한 패키지가 앞에서 임포트한 것을 가린다.

    Ex. StringBuilder 클래스는 scala 패키지와 java.lang 패키지에 있지만 scala.StringBuilder을 가리킨다.

 

 

13.5 접근 수식자

: 스칼라는 패키지, 클래스, 객체 멤버 앞에 private와 protected 접근 수식자를 두어 멤버에 대한 접근을 제한할 수 있다.

 

< 비공개 멤버 >

: 자바와 유사하지만 아래와 같은 예외 존재(자바는 외부 클래스가 자신의 내부 클래스에 있는 비공개 멤버에 접근 가능)

class Outer {
  class Inner {
      private def f() = { println("f") }
      class InnerMost {
          f() // OK
       }
   }
   (new Inner).f() // error: f is not accessible
}

 

< 보호 멤버 >

: 보호 멤버를 정의한 클래스의 서브 클래스에서만 멤버 접근 가능(자바보다 엄격)

package p {
   class Super {
      protected def f() = { println("f") }
   }
   class Sub extends Super {
      f()
   }
   class Other {
      (new Super).f() // error: f is not accessible
   }
}

 

< 공개 멤버 >

: private나 protected가 없는 멤버, public 수식자 없음

 

< 보호 스코프 >

: private[X], protected[X] 형식

: X라는 지정자를 통해 접근이 X까지 비공개이거나 보호

: X는 패키지, 클래스, 싱글톤 객체

 

package bobsrockets

package navigation {
   private[bobsrockets] class Navigator {       // 1
      protected[navigation] def useStarChart() = {}    // 4
   

       class LegOfJourney {
          private[Navigator] val distance = 100   // 2
       }
      private[this] var speed = 200   // 5
   }
}

 

package launch {
   import navigation._
   object Vehicle {
         private[launch] val guide = new Navigator    // 3
   }
}

1. Navigator은 bobsrockets 패키지에 있는 모든 객체와 클래스에서 접근 가능 => Vehicle 객체 내부에서 접근 가능

2. distance는 Navigator 클래스 내부 어디서나 접근 가능 == 자바 내부 클래스의 비공개 멤버와 동일한 접근 제어

3. guide 는 lanuch 패키지 내부 어디서든 접근 가능

4. useStarChart는 Navigator의 모든 서브 클래스 내부와 navigation 패키지에 있는 모든 객체, 클래스에서 접근 가능

5. private[this](객체 비공개)는 그 정의를 포함하는 객체 내부에서만 접근 가능, private보다 제한

 

class Foo {

    private[this] def isFoo = true

    def doFoo(other: Foo)  {

            if (other.isFoo) { // this line won't compile //

                ...

            }

         }

}

class Foo {

   private def isFoo = true

   def doFoo(other: Foo) {

       if (other.isFoo) { // this now compiles //

              ...

        }

    }

}

=> 같은 클래스의 다른 객체에서 접근하지 않음을 보장

 

 

< 가시성과 동반 객체 >

: 객체는 자신의 동반 클래스와 모든 접근 권리를 공유, 역도 마찬가지(클래스가 동반 객체의 비공개 멤버에 모두 접근할 수 있는 것처럼 객체도 동반 클래스의 모든 비공개 멤버에 접근할 수 있다.)

class Rocket {
    import Rocket.fuel
    private def canGoHomeAgain = fuel > 20
}


object Rocket {
    private def fuel = 10
    def chooseStrategy(rocket: Rocket) = {
        if (rocket.canGoHomeAgain)
             goHome()
        else
             pickAStar()
    }
    def goHome() = {}
    def pickAStar() = {}
}

: 싱글톤 객체는 서브 클래스를 만들 수가 없으므로 동반 객체 안에서 보호 멤버를 선언 X

 

 

< 패키지 객체 >

: 패키지 객체를 통해 패키지 내부 최상위 수준에 메소드를 정의할 수 있다. (global 메소드를 만들겠다.)

: 패키지 객체 내부에 있는 모든 정의는 패키지 자체에 속한 멤버로 취급

// In file bobsdelights/package.scala 
package object bobsdelights {
   def showFruit(fruit: Fruit) = {
      import fruit._
      println(name + "s are " + color)
   }
}


// In file PrintMenu.scala
package printmenu
import bobsdelights.Fruits
import bobsdelights.showFruit

 

object PrintMenu {
   def main(args: Array[String]) = {
          for (fruit <- Fruits.menu) {
              showFruit(fruit)
          }
    }
}

: 패키지 내에서 사용할 타입 별명과 암시적 변환을 넣기 위해 패키지 객체 사용(20장, 21장)

: 패키지 객체는 package.class 라는 클래스 이름으로 컴파일

: 컴파일된 클래스 파일은 패키지 클래스와 대응하는 패키지 디렉토리에 들어간다. 이 관례를 소스 파일에도 적용하는 것이 좋다.(bobsdelights 패키지 객체의 소스코드를 bobsdelights 디렉토리에 있는 package.scala로 저장)

+ Recent posts