[2016]올해를 마치며.. 회고!

2016 올해도 하루밖에 남지 않았다. 예전에 어른들이 나이가 들면 들수록 시간이 화살처럼

쏜살같이 지나간다는 말을 했었는데 마흔줄로 들어가고 있는 요즘 그걸 조금씩 실감하고 있다.

정신없이 지나갔지만 나름 열심히 달려왔던 2016 병신년 한해를 회고해본다.


1. 도전!!

올 한해를 보내며 스스로 가장 기억에 남는 건 “실시간 데이터 처리를 위한 인프라 구성과

관련 프로젝트를 진행한 것“이다.

openstackzookeeper, kafka, storm등의 기술을 이용해 클라우드를 구성하는 것부터가

경험이 없는 나로선 쉽지 않은 도전이었다.

다행히 사내 인프라가 잘 갖춰져 있었고 모르는 부분에 대해서 조언해줄 수 있는 동료가

곳곳에 있었기 때문에 가능한 일이었다고 생각한다.

프로젝트 초반엔 ansible의 역할이 참 컸는데, 다수의 가상 인스턴스를 가지고 클라우드를

세팅/운영하는데 있어 단시간에 많은 일들을 할 수 있었다.

kafka로 데이터를 보내는 역할(producer)은 logstash를 이용했고, storm에서 소비한

데이터는  사전에 설계한 HBase 테이블에 저장해 서비스에 활용했다.

그리고 운영시 storm supervisor 인스턴스를 늘려도 원하던 성능이 나오지 않아 고민하던 때,

storm 커미터이자 PMC 멤버인 임정택님의 도움을 받아 여러 세팅관련 조언을 듣고

운영상 어려움을 해결 할 수 있었다.

여담으로, storm topology  배포시 로컬과 운영 클라우드 환경이 다르기 때문에 꼭 서비스 전

운영 클라우드 환경에 배포해 테스트 해보는 것이 좋다.

그리고 데이터를 소비하는 어플리케이션 개발 특성상 테스트코드를 만들때

spockData Driven Testing을 이용해 보는 것도 좋을 듯 싶다.

서비스 어플리케이션 중 하나는 사내 mesos, marathon 인프라 위에 docker image를 만들어

배포하고 운영했는데 이것도 전에 없던 색다른 경험이었다.

2. 프로그래밍 언어

올해 단 한줄이라도 코드를 만들어봤던 프로그래밍 언어를 한번 나열해봤다.

java, groovy, golang, python, clojure, R, perl, shell script

예전에 비해 직/간접적으로 다뤄봤던 언어의 스펙트럼이 많이 넓어졌다.

그로 인해 자바의 세계에만 갇혀있던 내 시야도 조금은 트이는 한해가 된 것 같다.

그리고 아무래도 현재 하고 있는 일이 백엔드쪽 일이고 상대적으로 프론트엔드에 대한 관심이

적어서인지 전회사에서 많이 사용했던 javascript는 조금씩 멀어진 듯 하다.

groovy는 spock과 gradle을 쓰며 많이 사용했고, 간단한 스크립트 작업에도 유용하게

사용했다. clojure는 회사동료 덕분에 접해볼 수 있었고, 나머지는 회사일이나

개인적으로 공부하며 잠깐씩 다뤄봤던 언어들이 대부분이다.

2017년엔 아마도 다음과 같은 언어들도 필요에 따라 다뤄볼 수 있을 듯 하다.

scala, kotlin, swift

3. 번역

올해 후반기때 새로 시작한게 “번역”인데, 영어를 잘해서 한 건 아니고 spock으로 테스트코드를

만들면서 junit과 비교해 상대적으로 더 쉽고 직관적이라 쓰기가 편했다.

올해 중반엔 최범균님의 DDD START!의 junit 테스트코드를 spock으로 포팅하며 연습했고,

이것이 결국 페이스북에 “테스트코드와 친해지기“라는 그룹을 만드는 계기가 됐다.

물론, java개발자라면 groovy로 테스트코드를 만들어야 하는 spock에 대한 심리적인 저항이

있을 수도 있어서, 그걸 완화시키기 위한 일환으로 번역을 시작했다.

시작 전, spock 공식문서를 번역해도 되냐고 spock github issue로 올렸는데 친절히

내 블로그에 올리는 게 좋겠다는 댓글이 달려서 자신감을 얻을 수 있었다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-12-31-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-10-46-31

아직 3개정도 문서를 더 번역해야 한다.

spock관련해서 다른 포스팅도 있으니 함께 참고하면 좋을 듯 싶다.

추가적으로, 예전에 “토비의 스프링”의 저자 이일민님이 공부해보라고 추천해주신

JUnit A Cook’s Tour을 번역하면서 내용에 나온 junit의 목표와 철학, 그리고 설계에

반영된 다양한 디자인패턴에 대해 알게되었다.

4. 스터디

올해 스터디를 했거나 현재 진행중인 것은 6개다.

특히 지금 안드로이드 스터디를 discord를 통해서 하고 있는데 시간에 제약이 있는 나로선

온라인으로 스터디시 굉장히 유용할 것 같아 내년에 적극 활용해 볼 생각이다.

5. 읽었던 책

올해 읽었던 책은 대략 20~30권 정도 되는 것 같다.

현재 회사의 도서 지원이 빵빵해서 책을 사서 보는 데 아무런 제약이 없는 것이

너무 좋다. 그런데 필요할 때마다 이것저것 봤던 터라 완독률은 많이 떨어졌다.

그리고 새로 생긴 습관이 예전엔 책을 처음부터 끝까지 정독했다면 지금은

책을 보기 전에 목차를 한번 쭉 훑어보고 내가 필요한 부분만 본다는 점이다.

양쪽 모두 장단점이 있지만 일단 현재로선 필요한 때마다 그때그때 보는 것을

선호하게 될 듯 싶다.

내 머리가 좋아서 동시에 여러권을 봐도 스펀지처럼 쫙쫙 흡수해서 실무에

활용하면 좋겠지만, 그건 애초에 불가능하니 집에다 모셔두면서 썩히기보단

차라리 완독률은 떨어져도 다독을 하는 것이 나에게 더 좋을 듯 싶다.

끝으로 올해 읽으면서 재밌던 책을 뽑아봤다.

아! 그러고 보니 올해는 기술관련된 책 이외의 양서는 한권도 보지 못했다(책 편식현상이 ;;)

6. 올해 아쉬운 점과 새로운 도전꺼리들

올해 아쉬운 점은 “개인 프로젝트를 하지 못한 것“과 “오픈소스 contribution“을 도전하지 못한

것이다. 둘다 실행에 옮기지 못한 이유는 아마도 해야될 절실함을 스스로 찾지 못해서이지

않을까 싶다.

지속적으로 책을 보고 인터넷이 널린 다양한 콘텐츠를 코드로 따라쳐가며 공부했지만,

회사일을 제외한 뭔가 나만의 코드가 없다는 공허함은 지울 수가 없다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-12-31-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-11-16-59

그래서 내년엔 위의 github contribution에 내 코드를 꽉꽉 채워넣을 생각이다.

아이템도 있으니 실행에만 옮기면 된다. 지속적으로 동기부여를 원활하게 할 만한 방법도

고민해 볼 것이다.

올해 고무적인 것은 Trello를 회사일과 개인적인 일에 적극 활용했다는 것인데,

특히 개인적으로 쓰는 것보다 회사 프로젝트에 사용했을 때 프로젝트의 진척사항이나

팀원별 할일 관리, 현재 이슈사항 파악등에서 큰 효과를 보았다.

그리고 무엇보다 할 일을 가능한 작게 쪼개서 해보니 생각보다 할일이 더 명확히 보이고

일의 진행도 더 원활하게 됨을 체험할 수 있었다.

내년엔 개인적으로도 더 적극적으로 활용해 볼 생각이다.

일을 완료하고 카드를 옮기는 재미가 생각보다 쏠쏠하다.

마지막으로, 현재 내 관심꺼리는 “데이터처리와 분석을 통한 추천“과 “모바일개발“인데

관심에 비해 실행과 결과물이 많이 없어서 내년엔 지속적인 공부와 더불어

뭐라도 하나 “쓸모있는 물건!“을 만드는 게 가장 큰 목표다.

Advertisements

[번역]Spock – Data Driven Testing

Spock의 공식문서 중 Data Driven Testing를 번역해 보았습니다.

보실 때 오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


흔히 다양한 입력 및 예상된 결과로 동일한 테스트 코드를 여러번 실행하는 것이 유용하다.
spock의 데이터 기반 테스팅 지원은 이것을 최고의 기능으로 만든다.

Introduction

Math.max 메서드의 동작을 지정한다고 가정하자:

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    // exercise math method for a few different inputs
    Math.max(1, 3) == 3
    Math.max(7, 4) == 7
    Math.max(0, 0) == 0
  }
}

이 접근법은 이와 같이 단순한 경우에는 좋지만, 몇가지 잠재적인 단점이 있다.

  • 코드와 데이터가 섞여 있어 쉽게 독립적으로 변경할 수 없다.
  • 데이터를 외부 소스에서 쉽게 자동생성하거나 가져올 수 없다.
  • 동일한 코드를 여러번 실행하려면, 중복되거나 별도의 메서드로 분리해야 한다.
  • 실패한 경우, 실패를 일으킨 입력값이 무엇인지 바로 알 수 없을 수도 있다.
  • 동일한 코드를 여러번 실행하는 것은 별도의 메서드를 실행하는 것과 동일한 격리로 인해
    이익을 얻지 못한다.

spock의 데이터 기반 테스팅 지원은 이러한 문제를 해결하려고 한다.
시작하려면, 코드를 data-driven feature method로 리팩토링하자.
첫번째, 하드코딩된 integer 값들을 대체할 세개의 메서드 파라미터(데이터 변수라고 불리는)를
소개한다.

class MathSpec extends Specification {
  def "maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c
    ...
  }
}

우리는 이 테스트로직을 끝냈지만, 여전히 사용할 데이터 값을 제공할 필요가 있다.
이것은 항상 메서드의 끝에 위치한 where: 블록에서 수행된다.
가장 단순한(가장 일반적인) 경우에, where: 블록은 데이터 테이블이 있다.


Data Tables

데이터 테이블은 고정된 값 집합을 사용하여 기능 메서드를 실행하는 편리한 방법이다.

class MathSpec extends Specification {
  def "maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c

    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }
}

테이블 헤더라 불리는 테이블의 첫번째 행은 데이터 변수를 선언한다. 테이블 행이라 불리는
다음 행은 해당하는 값이 있다. 각 행에 대해 기능 메서드는 한번씩 실행된다.
이것을 메서드의 반복이라 부른다. 반복이 실패하더라도, 나머지 반복은 실행될 것이다.
모든 실패는 보고될 것이다.

데이터 테이블은 적어도 두개의 열이 있어야 한다. 단일 열 테이블은 다음과 같이 작성할 수 있다.:

where:
a | _
1 | _
7 | _
0 | _

Isolated Execution of Iterations

반복은 별도 기능 메서드와 동일한 방법으로 서로 분리된다. 각 반복은 specification class의
자체 인스턴스를 가져오고, setup과 cleanup 메서드는 각각 반복 전후에 호출된다.


Sharing of Objects between Iterations

반복간에 객체를 공유하려면, @Shared 또는 static 필드에 보관해야 한다.

NOTE  | @Shared와 static 변수는 where: 블록내에서 접근할 수 있다.

이러한 객체는 다른 메서드와도 공유된다.  현재 동일한 메서드의 반복 사이에서 객체를 공유하는
좋은 방법은 없다.  이 문제를 고려한다면, 각 메서드를 별도 스펙에 넣는 것을 고려하라, 모든 스펙은
동일한 파일에 보관될 수 있다. 이것은 일부 상용구 코드를 사용하여 더 나은 격리를 얻을 수 있다.


Syntactic Variations

이전 코드는 몇가지 방법으로 조정할 수 있다.
첫째, where: 블록은 이미 모든 데이터 변수를 선언하므로, 메서드 파라미터는 생략할 수 있다.
둘째, 입력과 예상된 출력은 이중 파이프 기호(||)를 사용하여 시각적으로 구분할 수 있다.
이것을 사용하면 코드는 다음과 같이 된다.

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
  }
}

Reporting of Failures

 max 메서드의 구현에 결함이 있어서 반복 중 하나가 실패한다고 가정하자.

maximum of two numbers   FAILED

Condition not satisfied:

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

분명한 질문은 어떤 반복이 실패했고 데이터 값은 무엇인가이다. 이 예에서는, 두번째 반복에서
실패했다는 것을 알기가 어렵다. 다른 경우에 이것은 더 어렵거나 불가능할 수도 있다.
어떤 경우엔, spock이 실패를 보고하는 것보다 반복이 실패하는 것을 크고 분명하게 만들면
좋을 것이다. 이것이 @Unrolling 어노테이션의 목적이다.


Method Unrolling

@Unroll로 어노테이션된 메서드는 반복이 독립적으로 보고된다.

@Unroll
def "maximum of two numbers"() {
...
@Unroll이 기본이 아닌 이유는 무엇인가?
@Unroll이 기본이 아닌 한가지 이유는 일부 실행환경(특히 IDEs)은 테스트 메서드의 수를 미리
알려주고 실제 수가 다를 경우 특정문제가 발생하길 기대하기 때문이다.

다른 이유는 @Unroll이 보고된 테스트 수를 크게 바꿀 수 있기 때문이고 이것은 항상 바람직하지 않다.
unrolling은 메서드 실행에 아무런 영향을 주지 않는다. 그것은 단지 보고의 대안일 뿐이다.
실행환경에 따라 출력은 다음과 같다.

maximum of two numbers[0]   PASSED
maximum of two numbers[1]   FAILED

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

maximum of two numbers[2]   PASSED

이것은 두번째 반복(인덱스 1 포함)이 실패했음을 알려준다. 조금만 더 노력한다면, 더 잘할 수 있다.

@Unroll
def "maximum of #a and #b is #c"() {
...

메서드 이름은 선행 해시 기호(#)로 표시된 placeholders를 사용하여 데이터 변수 a, b 및 c를
나타낸다. 결과에서 placeholders는 구체적인 값으로 변경된다.

maximum of 3 and 5 is 5   PASSED
maximum of 7 and 0 is 7   FAILED

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

maximum of 0 and 0 is 0   PASSED

이제 우리는 max 메서드 입력 7과 0에 대해 실패한 것을 한 눈에 알 수 있다.
자세한 내용은 More on Unrolled Method Names를 참조하라

@Unroll 어노테이션은 스펙에 배치할 수도 있다. 이는 스펙의 각 데이터 주도 기능 메서드에
배치하는 것과 동일한 효과를 발휘한다.


Data Pipes

데이터 테이블은 데이터 변수에 값을 제공하는 유일한 방법은 아니다. 사실, 데이터 테이블은
하나이상의 데이터 파이프에 대한 syntactic sugar이다.

...
where:
a << [1, 7, 0]
b << [3, 4, 0]
c << [3, 7, 0]

왼쪽 시프트 연산자(<<)로 표시된 데이터 파이프 연산자는 데이터 변수를 데이터 공급자에게 연결한다.
데이터 공급자는 반복마다 하나씩 변수에 대한 모든 값을 보유한다.
반복하는 방법을 알고 있는 Groovy의 어떠한 객체도 데이터 공급자로 사용될 수 있다.
이것은 Collection, String, Iterable 및 Iterable 계약을 구현한 객체를 포함한다.
데이터 공급자가 반드시 데이터일 필요는 없다(Collection의 경우에)
그들은 텍스트 파일, 데이터베이스, 스프레드시트같은 외부 소스에서 데이터를 가져오거나
임의로 데이터를 생성할 수 있다. 데이터 공급자는 필요할 때만(다음 반복전에) 다음 값으로 질문된다.


Multi-Variable Data Pipes

데이터 공급자가 반복당 여러 값(Groovy가 반복하는 방법을 알고 있는 객체)을 반환하면,
동시에 여러 데이터 변수에 연결할 수 있다. 문법은 Groovy multi-assignment와 비슷하지만
왼쪽에는 parentheses 대신 brackets를 사용한다.

@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}

관심이 없는 데이터는 밑줄(_)로 무시할 수 있다.

...
where:
[a, b, _, c] << sql.rows("select * from maxdata")

Data Variable Assignment

데이터 변수에는 직접 값을 할당할 수 있다:

...
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b

할당은 모든 반복마다 재평가된다. 위에 표시한 것처럼, 할당의 오른쪽은 다른 데이터 변수를
참조할 수 있다.

...
where:

where:
row << sql.rows("select * from maxdata")
// pick apart columns
a = row.a
b = row.b
c = row.c

Combining Data Tables, Data Pipes, and Variable Assignments

필요에 따라 데이터 테이블, 데이터 파이프 및 변수 할당을 결합할 수 있다.

...
where:
a | _
3 | _
7 | _
0 | _

b << [5, 0, 0]

c = a > b ? a : b

Number of Iterations

반복횟수는 사용가능한 데이터 양에 따라 다르다. 동일한 메서드를 연속적으로 실행하면
반복 횟수가 달라질 수 있다.데이터 공급자의 peer가 다른 값보다 빨리 실행되면,
예외가 발생할 것이다. 변수 할당은 반복횟수에 영향을 주지 않는다.
where: 블록은 할당이 하나만 있으면 정확히 하나의 반복을 만든다.


Closing of Data Providers

모든 반복이 완료된 후, 인자가 없는 close 메서드는 이 메서드가 있는 모든 데이터 제공자에서
호출된다.


More on Unrolled Method Names

 unroll 메서드 이름은 Groovy GString과 비슷하지만, 다음과 같은 차이가 있다.

  • 표현식은 $대신에 #으로 표시되고, ${…} 문법에는 해당되지 않는다.
  • 표현식은 속성 접근 및 인자가 없는 메서드 호출만 지원한다.

이름과 나이 속성을 가진 Person 클래스와 Person 타입의 person 데이터 변수가 주어지면
유효한 메서드 이름은 다음과 같다.

def "#person is #person.age years old"() { // property access
def "#person.name.toUpperCase()"() { // zero-arg method call

위의 #person과 같은 String 아닌 값은 Groovy semantics에 따라 String으로 변환된다.

다음은 잘못된 메서드 이름이다.

def "#person.name.split(' ')[1]" {  // cannot have method arguments
def "#person.age / 2" {  // cannot use operators

필요하다면, 복잡한 표현식을 유지하기 위해 추가 데이터 변수를 도입할 필요가 있다.

def "#lastName"() { // zero-arg method call
  ...
  where:
  person << [new Person(age: 14, name: 'Phil Cole')]
  lastName = person.name.split(' ')[1]
}

[번역]JUnit A Cook’s Tour

예전에 토비의 스프링의 저자 이일민님에게 뜬금없이 아래와 같은 질문을 한 적이 있습니다.

스프링 프레임워크 소스를 분석하면서 객체지향 공부를 하려고 하는데 어떻게 해야 할까요?

이에 대한 답으로 JUnit A Cook’s Tour를 읽어보라고 하셔서 공부할 겸 번역해 보았습니다.

오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


Note : 이 기사는 Junit 3.8.x를 기반으로 한다.


1. Introduction

이전 기사에서 우리는 간단한 프레임워크를 사용해 반복가능한 테스트를 만드는 방법에 대해 설명했다.
이번 기사에서 우리는 프레임워크를 살펴보고 그것이 어떻게 구성되었는지 보여준다.

우리는 JUnit 프레임워크를 주의깊게 연구하고 우리가 그것을 어떻게 구성했는지를 반영했다.
우리는 다양한 수준에서 교훈을 발견했다. 이 기사에서 우리는 한 번에 모든 것을 전달하려고
노력하겠지만, 적어도 검증된 가치를 지닌 소프트웨어의 설계 및 구성을 보여주는 맥락에서
이를 진행할 것이다.

우리는 프레임 워크의 목표에 대한 토론으로 시작한다. 목표는 프레임워크 자체가 보여지는 동안 많은 작은 세부 사항에서 다시 나타난다. 이후, 우리는 프레임워크의 설계와 구현을 제시한다.
설계는 세련된프로그램으로 구현된 패턴(놀람, 놀라움)의 관점에서 설명 될 것이다.
우리는 프레임워크 개발에 대한 몇 가지 선택적인 생각으로 결론을 맺는다.

2. Goals

Junit의 목표는 무엇인가?

첫째,  개발시 다음과 같은 가정을 해보자. 프로그램 기능에 자동화 된 테스트가없는 경우 작동하지 않는다고 가정한다. 개발자가 우리에게 프로그램 기능이 작동한다는 것을 보증한다면 현재와 영원히 작동한다는 일반적인 가정보다 훨씬 안전하다.

이 관점에서 개발자는 코드를 만들거나 디버깅할 때 이것이 이뤄지지 않으면, 프로그램이 동작함을 입증하는 테스트들을 만들어야 한다. 그럼에도 불구하고, 모두는 바쁘고, 그들은 할일이 많고,  충분한 시간을 할애하지 못한 채 테스트를 하며 보낸다. 난 이미 너무 많은 코드를 작성했는데 어떻게 또 테스트코드를 만들까요? 대답해주세요. Mr. Hard-case Project Manager.

그래서 목표 중 하나는 개발자들이 실제로 테스트코드를 만들 희망이 조금 있는 프레임워크를 만드는 것이다. 프레임워크는 친숙한 도구를 사용하므로 새로 익힐 것이 거의 없다. 그것은 테스트를 만드는 데 절대적으로 필요한 것 이상의 작업을 요구하면 안된다. 그것은 중복된 노력을 제거해준다.

모든 테스트들이 동작해야 할 경우, 디버거에 표현식을 작성하면 된다. 그러나 이것은 테스트를 하기에 충분하지 않다. 당신의 프로그램 작동이 나에게 도움이 되지 않는다. 왜냐하면 그것은 통합이 끝난 뒤 1분 후에도 프로그램이 정상작동하리라고 보증하지 않기 때문이다. 그리고 그것은 당신이 오랫동안 자리를 비웠을 때 5년뒤에도 정상적으로 동작한다고 보증하지 못한다.

그래서 테스트의 두번째 목표는 시간이 지나도 가치를 유지하는 테스트를 만드는 것이다. 때때로 테스트를 만든 사람이 아닌 다른 사람이 테스트를 실행하고 결과를 해석할 수 있어야 한다.  다양한 개발자들의 테스트를 결합하고 간섭의 두려움없이 그것들을 함께 실행할 수 있어야 한다.

마지막으로,  기존 테스트를 활용하여 새로운 테스트를 만들 수 있어야 한다. setup이나 fixture를 만드는 것은 값비싼 비용이 들고 프레임워크는 다른 테스트들을 실행하기 위해 fixture들을 재사용할 수 있어야 한다. 오, 그게 다야?

3. The Design of JUnit

JUnit의 디자인은 (“패턴 생성 아키텍처”, Kent Beck 및 Ralph Johnson, ECOOP 94 참조)에서 처음 사용된 스타일로 제공된다.  아이디어는 시스템의 아키텍처를 가질 때까지 아무것도 시작하지 않고 하나하나씩 패턴을 적용하여 시스템 설계를 설명하는 것이다. 우리는 해결할 아키텍처 문제를 제시하고, 이를 해결하는 패턴을 요약한 다음 패턴이 JUnit에 어떻게 적용되었는지 보여줄 것이다.

3.1 Getting started- TestCase

먼저 기본개념인 TestCase를 표현할 객체를 만들어야 한다.
개발자는 종종 테스트 사례를 염두에 두지만 여러 가지 방법으로 테스트 사례를 실현한다.

  • 구문 출력
  • 디버거 표현식
  • 테스트 스크립트들

조작이 쉬운 테스트를 만들고 싶다면 객체를 만들어야 한다.
이는 은연중에 개발자가 테스트를 받아들이고 구체적으로 만들어, 시간이 지남에 따라 가치를 유지하는 테스트를 만드는 목표를 뒷받침한다.
동시에 객체 개발자는 객체 개발에 익숙해지기 때문에 객체로 테스트를 하는 결정은 테스트 작성을
더 매력적으로 만드는 (또는 적어도 덜 강요하는) 목표를 뒷받침한다.
커맨드 패턴 (see Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995)은 우리의 필요를 아주 잘 맞춘다.
의도를 인용하면 “요청을 객체로 캡슐화하여 큐 또는 로그 요청 …”명령은 작업에 대한 객체를 만들고 “execute”메소드를 제공한다. 다음은 TestCase의 클래스 정의 코드이다.

public abstract class TestCase implements Test {
    …
}

이 클래스는 상속을 통해 재사용 될 것으로 기대하기 때문에 “public abstract”이라고 선언한다.
지금은 Test 인터페이스를 구현한다는 사실을 무시하라. 현재 설계의 목적을 위해 TestCase를
하나의 class로 생각할 수 있다.
모든 TestCase는 이름으로 작성되므로 테스트가 실패하면 실패한 테스트를 식별 할 수 있다.

public abstract class TestCase implements Test {
    private final String fName;

    public TestCase(String name) {
        fName= name;
    }

    public abstract void run();
        …
}

JUnit의 발전을 설명하기 위해, 아키텍처의 스냅 샷을 보여주는 다이어그램을 사용한다.
우리가 사용하는 표기법은 간단하다. 연관된 패턴을 포함하는 음영 처리된 상자로 클래스에 주석을
붙인다. 패턴에서 클래스의 역할이 분명하면 패턴이름만 표시된다. 역할이 명확하지 않은 경우
음영 처리된 상자는이 클래스가 해당하는 참가자의 이름으로 보강된다.
이 표기법은 다이어그램의 혼란을 최소화하기 위해 (see Gamma, E., Applying Design Patterns in Java, in Java Gems, SIGS Reference Library, 1997)에 처음으로 나타났다.
그림 1은 TestCase에 적용된 표기법을 보여준다. 우리는 하나의 클래스를 다루고 있으므로
모호하지 않고 패턴이름만 보여진다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-12-03-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-10-20-22

3.2 Blanks to fill in- run()

해결해야 할 다음 문제는 개발자에게 fixture 코드와 테스트 코드를 넣는 편리한 장소를
제공하는 것이다. abstract로 TestCase를 선언하면 개발자는 subclassing으로
TestCase를 재사용하는 것으로 예상한다. 그럼에도, superclass에 하나의 변수가 있고
기능이 제공되지 않는다면 테스트를 쉽게 만드는 우리의 첫번째 목표를 충족하지 못한다.

다행히도, 모든 테스트에 test fixture를 설정하고 fixture에 대한 코드를 실행하고
결과를 확인한 다음, fixture를 정리하는 공통적인 구조가 있다.
이것은 각 테스트가 새로운 fixture로 실행되고 한 테스트의 결과가 다른 테스트의
결과에 영향을 줄 수 없다는 것을 의미한다.
이것은 테스트의 가치를 극대화하는 목표를 지원한다.

템플릿 메서드는 우리의 문제를 아주 잘 해결해준다. 의도로부터 인용하자면,
“연산에서 알고리즘의 골격을 정의하고, subclasses로 일부 단계를 연기한다.
템플릿 메서드는 알고리즘 구조를 변경하지 않고 subclasses가 알고리즘의
특정단계를 재정의할 수 있다” 이것은 정확히 맞다.
우리는 개발자가 fixture(set up and tear down) 코드를 만드는 방법과
테스트 코드를 만드는 방법을 개별적으로 고려할 수 있기를 바란다.
그럼에도, 이 순서의 실행은 fixture 코드나 테스트 코드를 만드는 방법과
관계없이 모든 테스트에서 동일하게 유지될 것이다.

여기 템플릿 메서드가 있다.

public void run() {
    setUp();
    runTest();
    tearDown();
}

이 메서드의 기본 구현은 아무것도 하지 않는다.

protected void runTest() {
}

protected void setUp() {
}

protected void tearDown() {
}

setUp과 tearDown은 재정의 될 것이지만, 그들은 프레임워크에 의해 호출될 것이므로
protected로 선언한다. 이번 여행의 두번째 스냅샷은 그림2에서 보여주고 있다.

Figure 2 TestCase.run() applies Template Method

3.3 Reporting results- TestResult

TestCase가 forest에서 실행되면 결과에 대해 아무도 신경쓰지 않는가?
물론, 당신은 테스트를 실행하여 실행여부를 확인해야 한다.
테스트가 실행된 후, 작동여부에 대한 요약을 담은 기록이 필요하다.

테스트가 성공 혹은 실패할 확률이 같거나 하나의 테스트만 실행한 경우,
TestCase 객체에 대한 flag를 설정하고 테스트 완료시 flag를 살펴볼 수 있다.
그럼에도, 테스트는 아주 불균형한데 보통은 작동한다.
따라서, 실패와 성공에 대해 간략히 압축한 요약정보를 기록하기를 원한다.

스몰토크 모범사례패턴은 (see Beck, K. Smalltalk Best Practice Patterns, Prentice Hall, 1996)
적용가능한 패턴을 가지고 있다. 이를 Collecting Parameter라고 부른다.
여러가지 방법으로 결과를 수집해야 할 때, 메서드에 파라미터를 추가하고 결과를 수집할
객체를 전달해야 한다. 테스트 결과를 수집하기 위해 TestResult라는 새로운 객체를 만들어야 한다.

public class TestResult extends Object {
    protected int fRunTests;

public TestResult() {
       fRunTests= 0;
    }
}

간단한 버전의 TestResult는 실행된 테스트 숫자만 계산한다.
이를 사용하려면, TestCase.rum() 메서드에 파라미터를 추가하고, TestResult에게
테스트가 실행중임을 알려라.

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    runTest();
    tearDown();
}

그리고 TestResult는 실행된 테스트의 숫자를 추적해야 한다.

public synchronized void startTest(Test test) {
    fRunTests++;
}

우리는 TestResult의 startTest메서드를 synchronized로 선언하여 테스트가 다른 쓰레드에서 실행될 때 단일 TestResult가 안전하게 결과를 수집할 수 있도록 한다.
마지막으로,  우리는 간단한 TestCase의 외부 인터페이스를 유지하기를 원하므로
자체적으로 TestResult를 만드는 파라미터 없는 run() 버전을 만든다.

public TestResult run() {
    TestResult result= createResult();
    run(result);
    return result;
}

protected TestResult createResult() {
    return new TestResult();
}

Figure 3 shows our next design snapshot.

Figure 3: TestResult applies Collecting Parameter

테스트가 항상 올바르게 실행되면, 테스트를 만들지 않아도 된다.
실패하는 테스트는 흥미로운데, 특히 실패할 것으로 예상하지 못하는 경우가 그렇다.
게다가 테스트는 예를 들어 잘못된 결과를 계산하여 예기치 못한 방식으로 실패할 수 있고,
예를 들어 배열의 범위를 벗어나게 테스트를 만들면 실패할 수 있다.
테스트가 실패하는 것과 상관없이 우리는 이어지는 테스트를 실행하길 바란다.

Junit은 실패와 오류를 구분한다. 실패의 가능성은 예상되고 assertions와 함께 확인된다.
오류는 ArrayIndexOutOfBoundsException과 같은 예상치 못한 문제이다.
실패는 AssertionFailedError 오류로 표시된다. 예상치 못한 오류와 실패를 구분하기 위해서
추가된 catch절 (1)에서 실패를 잡는다.
두번째 절 (2)에선 모든 다른 예외를 잡아서 테스트가 계속 실행됨을 보장한다.

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    try {
        runTest();
    }
    catch (AssertionFailedError e) { //1
        result.addFailure(this, e);
    }    catch (Throwable e) { // 2
        result.addError(this, e);
    }
    finally {
        tearDown();
    }
}

AssertionFailedError는 TestCase에서 제공하는 assertion 메서드에 의해 유발된다.
Junit은 서로 다른 목적을 위해 assertion 메서드의 세트를 제공한다.
여기 가장 간단한 방법이 있다.

protected void assertTrue(boolean condition) {
    if (!condition)
        throw new AssertionFailedError();
}

AssertionFailedError는  클라이언트(TestCase내부의 테스트 메서드)에 의해 잡히진 않지만
Template Method TestCase.run() 내부에서 발견할 수 있다.
따라서 Error에서 AssertionFailedError를 파생시킨다.

public class AssertionFailedError extends Error {
    public AssertionFailedError () {}
}

TestResult에서 오류를 수집하는 방법은 아래와 같다.

public synchronized void addError(Test test, Throwable t) {
    fErrors.addElement(new TestFailure(test, t));
}

public synchronized void addFailure(Test test, Throwable t) {
    fFailures.addElement(new TestFailure(test, t));
}

TestFailure는 실패한 테스트와 나중에 보고를 위한 예외를 함께 묶은 프레임워크 내부
helper class이다.

public class TestFailure extends Object {
    protected Test fFailedTest;
    protected Throwable fThrownException;
}

collecting parameter의 정규형은 각 메서드에 collecting parameter를 전달해야 한다.
이 조언에 따르면, 테스트 메서드 각각은 TestResult를 위한 파라미터가 필요하다.
이것은 이러한 메서드 signatures의 “오염”의 결과가 된다.
실패에 예외를 사용하는 친절한 부작용으로 이러한 signature 오염을 피할 수 있다.
테스트 케이스 메서드 혹은 그것으로부터 호출된 helper 메서드는 TestResult를 알 필요없이
예외를 던질 수 있다. 여기 refresher는 우리의 MoneyTest suite의 샘플 테스트 메서드이다.
그것은 테스트 메서드가 TestResult에 대해 알 필요가 없다는 것을 보여준다.

public void testMoneyEquals() {
    assertTrue(!f12CHF.equals(null));
    assertEquals(f12CHF, f12CHF);
    assertEquals(f12CHF, new Money(12, “CHF”));
    assertTrue(!f12CHF.equals(f14CHF));
}

JUnit은 TestResult의 다른 구현이 있다. 기본 구현은 실패 및 오류 숫자를 계산하고 결과를 수집한다.
TextTestResult는 결과를 수집하여 텍스트 형식으로 표시한다.
마지막으로, UITestResult는 그래픽 테스트 상태를 업데이트 하기 위해 JUnit Test Runner의
그래픽 버전을 사용한다.

TestResult는 프레임워크의 확장 지점이다. 클라이언트는 자신만의 TestResult  클래스를
정의할 수 있다. 예를 들어 HTMLTestResult는 HTML문서로 결과를 보여준다.

3.4 No stupid subclasses – TestCase again

우리는 Command를 테스트에 적용했다. Command는 execute(TestCase안에 run()이라 불리는)
와 같은 단일 메서드를 사용하여 호출한다. 이 간단한 인터페이스는 동일한 인터페이스를 통해
Command의 다른 구현을 호출할 수 있다.

우리는 일반적으로 테스트를 실행하기 위한 인터페이스가 필요하다.
그러나, 모든 테스트 케이스는 동일한 클래스에서 다른 메서드로 구현된다.
이것은 클래스의 불필요한 확산을 피하게 한다.
주어진 테스트 케이스는 여러 메서드를 구현할 수 있고, 각각은 하나의 테스트 케이스를 정의한다.
각 테스트케이스는 testMoneyEquals나 testMoneyAdd와 같은 설명이 포함된다.
이 테스트 케이스는 간단한 command 인터페이스를 따르지 않는다. 동일한 command 클래스의
다른 인스턴스는 다른 메서드로 호출해야 한다. 그러므로 우리의 다음 문제는 테스트 호출자의
관점에서 모든 테스트 케이스가 동일하게 보이도록 하는 것이다.

사용가능한 디자인 패턴으로 해결된 문제를 검토하면 Adapter 패턴이 마음에 든다.
Adapter에는 “클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환”이라는
의도가 담겨있다. 이것은 잘 맞는 것처럼 보인다. Adapter는 이 작업을 수행하는 다른 방법들을
알려준다. 그 중 하나는 subclassing을 사용하여 인터페이스를 조정하는 클래스 adapter이다.
예를 들어, testMoneyEquals를 runTest에 적용하려면 MoneyTest의 하위클래스를 구현하고runTest를 재정의하여 testMoneyEquals를 호출한다.

public class TestMoneyEquals extends MoneyTest {
    public TestMoneyEquals() { super(“testMoneyEquals”); }
    protected void runTest () { testMoneyEquals(); }
}

하위클래스를 사용하려면 각 테스트 케이스에 대한 하위클래스를 구현해야 한다.
이것은 테스트에게 추가적인 부담을 준다. 이것은 프레임워크가 테스트케이스를 추가하는 것을
가능한 단순하게 해야한다는 JUnit목표에 반하는 것이다. 또한, 각 테스트 메서드에 대한
하위클래스를 만들면 클래스가 늘어난다. 단일 메서드를 가진 많은 클래스들은 그 비용에 대한
가치가 없고 의미있는 이름을 찾기가 어려울 것이다.

Java는 익명내부클래스를 제공해서 클래스이름 지정 문제에 대한 흥미로운 java관련 솔루션을
제공한다. 익명 내부클래스를 사용하면 클래스 이름을 만들 필요없이 Adapter를 만들 수 있다.

TestCase test= new MoneyTest(“testMoneyEquals “) {
    protected void runTest() { testMoneyEquals(); }
};

이것은 완전한 서브클래스보다 더 편리하다. 그것은 개발자에게 약간의 부담을 주면서 컴파일타임
타입 체크를 한다. 스몰토크 모범사례 패턴은 pluggable 행동의 공통적인 표제하에 다르게
행동하는 서로 다른 인스턴스의 문제에 대한 또 다른 솔루션을 설명한다.
아이디어는 서브클래스를 필요로 하지 않고 다른 로직을 수행하기 위해 매개변수화 될 수 있는
단일 클래스를 사용하는 것이다.

pluggable 행동의 가장 간단한 형태는 Pluggable Selector이다. Pluggable Selector 는
인스턴스 변수에 스몰토크 메서드 선택자를 저장한다. 이 아이디어는 스몰토크에 국한되지
않는다. 그것은 자바에도 적용된다.  자바에선 메서드 선택자에 대한 개념이 없다.
그러나, 자바 reflection API를 사용하면 메서드 이름을 나타내는 문자열에서 메서드를 호출
할 수 있다. 이 기능을 사용하여 자바에서 pluggable selector를 구현할 수 있다.
여담이지만, 우리는 일반적으로 어플리케이션 코드에서 reflection을 사용하지 않는다.
우리의 경우에 인프라 프레임워크를 다루고 있으므로 reflection 모자를 쓰는 것이 좋다.

JUnit은 클라이언트에게 pluggable 선택자를 사용하거나 혹은 익명 adapter 클래스를 구현
할 수 있는 옵션을 제공한다. 이렇게 하기 위해, 우리는 pluggable 선택자를 runTest 메서드의
기본 구현으로 제공한다. 이 경우 테스트 케이스 이름은 테스트 메서드의 이름과 일치해야 한다.
다음과 같이 리플렉션을 사용하여 메서드를 호출한다. 먼저, 메서드 객체를 찾는다.
메서드 객체가 생기면 그것을 호출하고 인자를 전달할 수 있다. 우리의 테스트 메서드는 인자가
없기 때문에 빈 인자 배열을 전달 할 수 있다.

protected void runTest() throws Throwable {
    Method runMethod= null;
    try {
        runMethod= getClass().getMethod(fName, new Class[0]);
    } catch (NoSuchMethodException e) {
        assertTrue(“Method \””+fName+”\” not found”, false);
    }
    try {
        runMethod.invoke(this, new Class[0]);
    }
    // catch InvocationTargetException and IllegalAccessException
}

The JDK 1.1 리플렉션 API는 public 메서드만 찾을 수 있다. 이러한 이유로 테스트 메서드는
public으로 선언해야 하고, 그렇지 않으면 NoSuchMethodException이 발생할 것이다.

다음은 Adapter와 Pluggable Selector가 추가된 디자인 스냅샷이다.

Figure 4: TestCase applies either Adapter with an anonymous inner class or Pluggable Selector

3.5 Don’t care about one or many – TestSuite

시스템 상태에 확신을 얻으려면 많은 테스트를 실행해야 한다.
지금까지 JUnit은 단일 테스트 케이스를 실행하고 TestResult로 결과를 보고했다.
우리의 다음 도전은 다양한 테스트를 실행하도록 확장하는것이다.
이 문제는 테스트 호출자가 하나 이상의 테스트 케이스를 실행하는지 여부를 신경 쓸 필요없이
쉽게 해결 할 수 있어야 한다. 이러한 상황을 벗어날 수 있는 가장 인기있는 패턴은 Composite이다.
의도를 인용하면 “객체들을 트리구조로 구성해서 부분-전체 계층구조로 표현하면 Composite를
통해 클라이언트는 개별 객체와 객체의 구성을 균일하게 처리할 수 있다”
부분-전체 계층구조의 요점은 여기에 있다. 우리는 suites of suites of suites of tests를 지원하려고 한다.

Composite의 다음 참가자를 소개한다

  • Component: 테스트와 상호작용하는 데 사용할 인터페이스를 선언한다.
  • Composite: 인터페이스를 구현하고 테스트 컬렉션을 유지 관리한다.
  • Leaf: Component 인터페이스를 따르는 composition의 테스트케이스를 나타낸다.

이 패턴은 단일 혹은 복합 객체에 대한 일반적인 인터페이스를 정의하는 추상클래스를 소개한다.
클래스의 기본 목적은 인터페이스를 정의하는 것이다. 자바에서 Composite을 적용할 때는
추상 클래스가 아닌 인터페이스를 정의하는 것을 더 선호한다. 인터페이스를 사용하면
테스트를 위해 JUnit을 특정 기본 클래스에 맡기지 않아도 된다. 테스트들은 이 인터페이스를
따르는 것이 전부다. 따라서 패턴 설명을 조정하고 테스트 인터페이스를 도입한다.

public interface Test {
    public abstract void run(TestResult result);
}

테스트 케이스는 Composite의 Leaf에 해당하며 위에 보았듯이 인터페이스를 구현한다.

다음으로 Composite 참가자를 소개한다. 우리는 TestSuite 클래스의 이름을 지정한다.
TestSuite는 Vector안에 자식 테스트들을 가지고 있다.

public class TestSuite implements Test {
    private Vector fTests= new Vector();
}

run()메서드는 자식에게 위임한다.

public void run(TestResult result) {
    for (Enumeration e= fTests.elements(); e.hasMoreElements(); ) {
        Test test= (Test)e.nextElement();
        test.run(result);
    }
}
Figure 5: TestSuite applies Composite

마지막으로, 클라이언트는 suite에 테스트를 추가할 수 있어야 하고, addTest 메서드로
그것을 할 수 있다.

public void addTest(Test test) {
    fTests.addElement(test);
}

위 코드는 모두 Test 인터페이스에만 의존하고 있다. TestCase와 TestSuite는 Test 인터페이스를
따르므로 test suites를 재귀적으로 구성할 수 있다.
모든 개발자는 자신의 TestSuites를 만들 수 있다.
우리는 저 suites로 구성된 TestSuite를 만들어서 실행할 수 있다.

여기 TestSuite를 만드는 예제가 있다.

public static Test suite() {
    TestSuite suite= new TestSuite();
    suite.addTest(new MoneyTest(“testMoneyEquals”));
    suite.addTest(new MoneyTest(“testSimpleAdd”));
}

이것은 정상적으로 작동하지만, 모든 테스트를 수동으로 suite에 추가해야 한다.
Junit의 얼리어댑터는 우리에게 이것이 바보같다고 말했다.
새로운 테스트 케이스를 만들때마다 정적 suite() 메서드에 그것을 추가해야 한다는 걸
기억해야 한다. 그렇지 않으면 그것은 실행되지 않을 것이다.
우리는 테스트 케이스 클래스를 인자로 가지는 편리한 생성자를 TestSuite에 추가했다.
그 목적은 테스트 메서드를 추출하고 이를 포함하는 suite를 만드는 것이다.
테스트 메서드들은 접두어 “test”로 시작하고 인자가 없는 간단한 규칙을 따라야 한다.
편리한 생성자는 이 규칙을 사용하여, 테스트 메서드를 찾는 리플렉션을 사용해서 테스트 객체를
생성한다. 이 생성자를 사용하면 위의 코드가 다음과 같이 단순화된다.

public static Test suite() {
    return new TestSuite(MoneyTest.class);
}

원래 방법은 테스트 케이스의 하위 집합을 실행하려는 경우에도 유용하다.

3.6 Summary

JUnit의 요리사 여행은 끝났다. 다음 그림은 패턴으로 설명된 JUnit의 디자인을 한 눈에 보여준다.

Figure 6: JUnit Patterns Summary

프레임워크의 추상화 중심인 TestCase가 4개의 디자인패턴과 어떻게 관련되어 있는지 주목하라.
성숙한 객체 디자인의 그림은 이와 동일한 “패턴 밀도”를 보여준다.
디자인의 스타는 지원하는 플레이어와 풍부한 관계를 맺고 있다.

JUnit의 모든 패턴을 보여주는 또 다른 방법이 여기 있다. 이 스토리보드에는 차례대로
각 패턴의 효과를 추상적으로 표현한 것을 볼 수 있다.
따라서 Command 패턴은 TestCase 클래스를 만들고, Template Method 패턴은
run 메서드를 생성한다.(스토리보드 표기법은 모든 텍스트가 삭제된 그림 6의 표기법이다)

Figure 7: JUnit Pattern Storyboard

스토리보드에 대해 알아야 될 점 중 하나는 Composite을 적용할 때 그림의 복잡성이 어떻게
증가하는지에 대한 것이다. 이것은 Composite이 강력한 패턴이라는 직관에 대해 그림으로
확증되었지만, 그것은 “그림을 복잡하게 한다”. 따라서 주의해서 사용해야 한다.

4. Conclusion

결론을 내리기 위해 몇가지 일반적인 관찰을 해보자

* Patterns

우리는 프레임워크를 개발할 때와 다른 사람에게 설명하려고 할 때 모두 패턴의 관점에서
디자인을 매우 중요하게 논의하는 것을 발견했다. 이제는 패턴이 있는 프레임워크를 설명하는
것이 효과적인지 판단할 수 있는 완벽한 위치에 있다. 위의 토론이 마음에 들면, 자신의
시스템에 동일한 스타일의 프리젠테이션을 시도해봐라.

* Pattern density

JUnit의 핵심 추상화인 TestCase의 주변에는 “밀도”패턴이 있다. 패턴 밀도가 높은 디자인은
사용하기 쉽지만 변경하기 어렵다. 핵심 추상화와 같은 높은 패턴밀도는 성숙한 프레임워크의
일반적이라는 사실을 발견했다. 미숙한 프레임워크에 대해서는 그 반대가 되어야 한다.
그들은 패턴의 밀도가 낮아야 한다. 실제로 어떤 문제를 발견하여 해결하고자 한다면,
솔루션을 압축하여 밀도를 높이고  밀도가 높은 패턴의 필드를 활용하게 할 수 있다.

* Eat your own dog food

기존 유닛 테스팅이 구현되자마자 우리는 그것을 적용했다. TestTest는 프레임워크가 오류,
성공, 실패에 대한 올바른 결과를 보고하는지 검증한다.
우리는 프레임워크의 디자인을 계속적으로 발전시켜 가면서 이 값진 것을 발견했다.
우리는 JUnit의 가장 어려운 어플리케이션이 자체 동작을 테스트하고 있다는 것을 발견했다.

* Intersection, not union

프레임워크 개발에는 가능한 모든 기능을 포함하려는 유혹이 있다. 결국, 프레임워크를 최대한
가치있게 만들고 싶다. 그러나 개발자가 프레임워크를 사용하기로 결정해야 하는 중립적인 힘이
있다. 프레임워크의 기능이 적을수록, 배우기 쉽고, 개발자가 사용할 가능성이 높다.
JUnit은 이 스타일로 작성되어있다. 테스트 실행에 절대적으로 필요한 기능만 구현한다
– 테스트 suites의 실행, 각 테스트 실행의 격리, 테스트 자동실행
물론 우리는 몇가지 기능을 추가하는 것을 거부할 수 없었지만, 우리는 그것을 자신의
확장패키지에(test.extensions)넣을 때 주의해야만 했다.
이 패키지의 주목할만한 구성요소는 테스트 전후에 추가코드를 실행할 수 있는
TestDecorator이다.

* Framework writers read their code

우리는 JUnit을 만드는 것보다 JUnit코드를 읽는 데 더 많은 시간을 썼고, 새로운 기능을
추가하는데 소요된 시간만큼 중복기능을 제거했다. 우리는 우리가 상상할 수 있는 다양한
방식으로 디자인을 적극적으로 실험하고, 새로운 클래스들을 추가하고, responsibility를
옮겼다. 우리는 JUnit에 대한 지속적인 통찰력(테스팅, 객체 설계, 프레임워크 개발 및 추가
기사의 기회)으로 우리의 monomania에 대한 보상을 받았따(그들은 여전히 보상을 받고 있다)

JUnit의 최신버전은 http://www.junit.org에서 다운받을 수 있다.

5. Acknowledgements

주의깊게 읽고 부드럽게 수정해준 John Vlissides, Ralph Johnson, and Nick Edgar에게 감사한다.