[테스트 주도 개발] 23장_얼마나 달콤한지

  • 테스트 메서드 호출하기
  • 먼저 setUp 호출하기
  • 나중에 tearDown 호출하기
  • 테스트 메서드가 실패해도 tearDown 호출하기
  • 여러 개의 테스트 실행하기
  • 수집된 결과를 출력하기
  • WasRun 에 로그 문자열 남기기
  • 실패한 테스트 보고하기
  • setUp 에러 잡아서 보고하기

테스트들을 모두 모아서 한 번에 실행하고 싶다. TestSuite 가 필요하다.

1
2
3
4
5
6
7
// TestCaseTest
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))
result = suite.run()
assert ("2 run, 1 failed" == result.summary())

add() 메서드를 구현은, 테스트들을 리스트에 추가하는 것으로 된다.

1
2
3
4
5
6
class TestSuite:
def __init__(self):
self.tests = []

def add(self, test):
self.tests.append(test)

run() 메서드는, 하나의 TestResult가 모든 테스트에 대해 쓰이길 바라기 때문에 다음과 같이 작성해야 한다.

1
2
3
4
5
6
// TestSuite
def run(self):
result = TestResult()
for test in self.tests:
test.run(result)
return result

만약 TestCase.run() 에 매개변수를 추가하면, TestSuite.run() 에도 똑같은 매개 변수를 추가해야 한다.

1
2
3
4
5
6
7
8
9
// TestCaseTest
def testSuite(self):
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.add(WasRun("testBrokenMethod"))

result = TestResult()
suite.run(result)
assert ("2 run, 1 failed" == result.summary())

이 방법은, run() 이 무언가를 명시적으로 반환하지 않아도 된다는 장점이 있다.

Read more

[테스트 주도 개발] 24장_xUnit 회고

xUnit을 사용하다 보면 assertion 의 실패와 나머지 종류의 에러 사이에 큰 차이점을 알게된다. 일반적으로 assertion failure가 디버깅 시간을 더 많이 잡아먹는다.
JUnit은 간단한 Test Interface 를 선언하는데 TestCase와 TestSuite 모두 이를 상속받는다. 만약 JUnit 도구가 테스트를 실행하게 만들고 싶으면, Test 인터페이스만 구현하면 된다.

1
2
3
4
public interface Test {
public abstract int countTestCases();
public abstract void run(TestResult result);
}

테스트 주도 개발 <켄트 벡>

Read more

[테스트 주도 개발] 18장_xUnit 으로 가는 첫걸음

2부는 테스트 주도 개발을 통해 테스트 프레임워크를 만들어 본다. 다음은 테스트 프레임워크에 대한 할일 목록이다.

  • 테스트 메서드 호출하기
  • 먼저 setUp 호출하기
  • 나중에 tearDown 호출하기
  • 테스트 메서드가 실패해도 tearDown 호출하기
  • 여러 개의 테스트 실행하기
  • 수집된 결과를 출력하기

먼저, 테스트 메서드가 호출되면 true, 그렇지 않으면 false 를 반환할 작은 프로그램이 필요하다. 플래그를 가진 테스트 케이스를 만들것이다. 테스트 메서드가 실행되기 전에는 플래그가 false 이다. 실행된 이후에는 true 이다. 메서드가 실행되었는지 알려주는 테스크 케이스이므로 클래스 이름은 WasRun 으로 하자.

1
2
3
4
test = WasRun("testMethod")
print test.wasRun
test.testMethod()
print test.wasRun

위 코드는, 메서드가 실행되기 전에는 “None” 을 출력, 그 후에 “1” 을 출력할 것으로 예상된다. 하지만, 아니다. 아직 WasRun 클래스를 정의하지 않았다.
WasRun 클래스 정의하자.

1
2
3
class WasRun:
def __init__(self, name):
self.wasRun = None

이제, 실행하면 “None” 을 출력하고, testMethod 를 정의해야한다고 알려준다.

정의하자.

1
2
3
4
5
6
class WasRun:
def __init__(self, name):
self.wasRun = None

def testMethod(self):
self.wasRun = 1

이제 필요한 것은, 테스트 메서드를 직접 호출하는 대신 인터페이스인 run() 메서드를 이용하는 것이다. 테스트는 다음과 같이 변한다.

Read more

[테스트 주도 개발] 19장_테이블 차리기

  • 테스트 메서드 호출하기
  • 먼저 setUp 호출하기
  • 나중에 tearDown 호출하기
  • 테스트 메서드가 실패해도 tearDown 호출하기
  • 여러 개의 테스트 실행하기
  • 수집된 결과를 출력하기

테스트 작성에는 다음 패턴이 있다.

  1. 준비 : 객체 생성
  2. 행동 : 어떤 자극 주기
  3. 확인 : 결과 검사

두, 세 번째는 항상 다르지만, 처음 단계는 여러 테스트에 걸쳐 동일한 경우가 있다.
테스트 커플링을 만들지 말자.
테스트 실행전에 우리는 어떤 플래그를 거짓으로 두기 원했다. 발전시키자.

1
2
3
4
5
6
7
8
9
10
class TestCaseTest(TestCase):
def setUp(self):
self.test = WasRun("testMethod")
def testRunning(self):
self.test.run()
assert(self.test.wasRun)
def testSetUp(self):
test = WasRun("testMethod")
test.run()
assert(self.test.wasSetUp)

wasSetUp 속성이 없으니 세팅하자.

1
2
3
class WasRun(TestCase):
def setUp(self):
self.wasSetUp = 1

setUp 호출하는 것은 TestCase 이니 이곳을 손보자.

1
2
3
4
5
6
7
8
9
10
11
class TestCase:
def __init__(self, name):
self.name = name

def setUp(self):
pass

def run(self):
self.setUp()
method = getattr(self, self.name)
method()

wasRun 플래그를 setUp 에서 설정하면 WasRun 을 단순화 할 수 있다.

Read more

[테스트 주도 개발] 20장_뒷정리하기

  • 테스트 메서드 호출하기
  • 먼저 setUp 호출하기
  • 나중에 tearDown 호출하기
  • 테스트 메서드가 실패해도 tearDown 호출하기
  • 여러 개의 테스트 실행하기
  • 수집된 결과를 출력하기
  • WasRun 에 로그 문자열 남기기

setUp() 은 테스트 메서드 실행되기 전에 호출, tearDown() 은 테스트 메서드가 실행된 후에 호출되어야한다.

1
2
3
4
5
class WasRun(TestCase):
def setUp(self):
self.wasRun= None
self.wasSetUp = 1
self.log = "setUp"

이제 testSetUp이 플래그 대신 로그를 검사하자.

1
2
3
4
class TestCaseTest(TestCase):
def testSetUp(self):
self.test.run()
assert("sepUp" == self.test.log)
1
2
3
4
class TestCaseTest(TestCase):
def testSetUp(self):
self.test.run()
assert("sepUp testMethod" == self.test.log)

이제 testSetUp 의 이름을 바꾸자.

1
2
3
4
5
class TestCaseTest(TestCase):
def testTemplateMethod(self):
teset = WasRun("testMethod")
test.run()
assert("sepUp testMethod" == self.test.log)
  • 테스트 메서드 호출하기
  • 먼저 setUp 호출하기
  • 나중에 tearDown 호출하기
  • 테스트 메서드가 실패해도 tearDown 호출하기
  • 여러 개의 테스트 실행하기
  • 수집된 결과를 출력하기
  • WasRun 에 로그 문자열 남기기

이제 tearDown() 을 테스트할 준비가 됐다.

Read more

[테스트 주도 개발] 17장_Money 회고

1. 다음에 할 일은 무엇인가

Sum.plus() 와 Money.plus() 사이에 중복이 남았다. Expressoin 을 인터페이스가 아니라 클래스가 바꾼다면 공통 코드를 담아낼 적절한 곳이 될 것이다.
작업을 끝낸 후에 SmallLint 같은 코드 감정 프로그램을 실행해보면 좋다.
“다음에 할일은 무엇인가” 에 관련된 또 다른 질문은 “어떤 테스트들이 추가로 필요할까” 이다.
할일 목록이 빌 때가 그때까지 설계한 것을 검토하기에 적절한 시기이다. 말과 개념이 잘 통하는가 ? 현재의 설꼐로 제거하기 힘든 중복이 있는가 ?

2. 메타포

“통화가 다른 여러 금전” 에 대해 사용한 메타포는 벡터였다. 그 전엔, MoneySum 을 사용하다가 적절하고 물리전인 MoneyBag 으로 바꿨다. 그리고 마지막에는 많은 사람에게 익숙한 Wallet 으로 바꿧다. 이 모든 메타포는 Money 의 집합이 딱 떨어지는숫자로 된다는 것을 암시하다. 즉, 같은 통화의 값은 합칠 수 있다. (2USD + 3USD + 5CHF = 5USD + 5CHF)
Expression 메타포는 중복되는 통화를 합치는 세세한 일단의 문제에서 해방시켰다. 코드도 그 어느 때보다 명확하다.

3. 코드 메트릭스

  1. 코드와 테스트 사이에 대략 비슷한 양의 함수와 줄이 있다.
  2. 테스트 코드에 분기나 반복문이 없기 때문에 테스트 복잡도는 1 이다. 명시적인 흐름 제어 대신에 다형성을 사용해서 실제 코드의 복잡도 역시 낮다.

4. 프로세스

TDD 의 주기는,

  1. 작은 테스트 추가
  2. 모든 테스트 실행, 실패 하는 것 확인
  3. 코드에 변화
  4. 모든 테스트 실행, 성공 하는 것 확인
  5. 중복 제거 위해 리펙토링

5. 테스트의 질

Read more

[테스트 주도 개발] 13장_진짜로 만들기

모든 중복을 제거하기 전에는 “5달러 + 5달러” 테스트는 끝난 것이 아니다. 코드 중복은 없지만, 가짜 구현에 있는 10 달러는 사실 테스트 코드에 있는 “5달러 + 5달러” 와 같다.

1
2
3
Money reduce(Expression source, String to) {
return Money.dollar(10);
}
1
2
3
4
5
6
7
8
@Test
void testSimpleAddition() {
Money five = Money.dollar(5);
Expression sum = five.plus(five); // here
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}

이전에는, 가짜 구현이 있을 때 진짜 구현으로 작업해가는 것이 명확했다. 이번에는 어떻게 거꾸로 작업해야할지 명확하지 않다. 그래서 조금 불확실하지만 순방향으로 작업해보자.
우선, Money.plus() 는 그냥 Money 가 아닌, Expression(Sum) 을 반환해야한다. 두 Money 의 합은 Sum 이어야한다.

1
2
3
4
5
6
7
8
@Test
void testPlusReturnsSum(){
Money five = Money.dollar(5);
Expression result = five.plus(five);
Sum sum = (Sum) result;
assertEquals(five, sum.augend);
assertEquals(five, sum.addend);
}

위 코드를 컴파일하기위해서는, augend, addend 필드를 가진 Sum 클래스가 필요하다.

1
2
3
4
class Sum {
Money augend;
Money addend;
}

다시 실행해보면, Money.plus() 는 Sum 이 아닌 Money 를 반환하게 되어 있어서, ClassCastExceptoin 을 발생시킨다. 그래서, 다음 처럼 수정하자.

1
2
3
4
// Money
Expression plus(Money addend){
return new Sum(this, addend)
}

Sum 생성자도 필요하다.

Read more

[테스트 주도 개발] 14장_바꾸기

이번에는 2프랑을 달러로 바꾸고 싶다.

1
2
3
4
5
6
7
@Test
void testReduceMoneyDifferentCurrency(){
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(Money.franc(2), "USD");
assertEquals(Money.dollar(1), result);
}

프랑을 달러로 바꿀때 나누기 2를 하자. 다음 코드를 추가하자.

1
2
3
4
5
6
// Money
public Money reduce(String to){
int rate = (currency.equals("CHF") && to.equals("USD")) ? 2 : 1;

return new Money(amount / rate, to);
}

환율에 대한 일은 모두 Bank 가 처리해야한다. Expression.reduce() 의 인자로 Bank 를 넘겨야할 것이다. 우선 호출하는 부분을 작성하자.

1
2
3
4
5
6
class Bank {

Money reduce(Expression source, String to) {
return source.reduce(this, to);
}
}

그리구 구현 부분,

1
2
3
interface Expression {
Money reduce(Bank bank, String to);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Sum implements Expression {
Money augend;
Money addend;

Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}

public Money reduce(Bank bank, String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}
}
1
2
3
4
5
6
// Money
public Money reduce(Bank bank, String to){
int rate = (currency.equals("CHF") && to.equals("USD")) ? 2 : 1;

return new Money(amount / rate, to);
}
Read more

[테스트 주도 개발] 15장_서로 다른 통화 더하기

이제 드디어, “5달러 + 10프랑” 테스트할 준비가 모두 되었다.
우리가 원하는 코드는,

1
2
3
4
5
6
7
8
9
10
11
@Test
void testMixedAddition() {
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(
fiveBucks.plus(tenFrancs), "USD"
);
assertEquals(Money.dollar(10), result);
}

컴파일 에러가 많다. 천천히 해결하자.

1
2
3
4
5
6
7
8
9
10
11
@Test
void testMixedAddition() {
Money fiveBucks = Money.dollar(5);
Money tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Money result = bank.reduce(
fiveBucks.plus(tenFrancs), "USD"
);
assertEquals(Money.dollar(10), result);
}

실패한다. 10 USD 대신, 15 USD 가 나왔다. Sum.reduce() 가 인자를 축약하지 않은 것으로 보인다. 다음과 같이 두 인자를 모두 축약하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sum implements Expression {
Money augend;
Money addend;

Sum(Money augend, Money addend) {
this.augend = augend;
this.addend = addend;
}

public Money reduce(Bank bank, String to) {
int amount = augend.reduce(bank, to).amount +
addend.reduce(bank, to).amount;
return new Money(amount, to);
}
}

그리고, Expression 이어야하는 Money 들을 조금씩 없앨 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sum implements Expression {
Expression augend;
Expression addend;

Sum(Expression augend, Expression addend) {
this.augend = augend;
this.addend = addend;
}

public Money reduce(Bank bank, String to) {
int amount = augend.reduce(bank, to).amount +
addend.reduce(bank, to).amount;
return new Money(amount, to);
}
}

Money 의 plus() 인자도 바꾸자.

1
2
3
Expression plus(Expression addend){
return new Sum(this, addend);
}
Read more

[테스트 주도 개발] 16장_드디어, 추상화

Expression.plus 를 끝내려면, Sum.plus() 를 구현해야한다. 그리고, Expression.times() 를 구현하면 끝난다. 다음은 Sum.plus() 테스트다.

1
2
3
4
5
6
7
8
9
10
11
@Test
void testSumPlusMoney() {
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression sum = new Sum(fiveBucks, tenFrancs).plus(fiveBucks);
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(15), result);

}

테스트가 코드보다 더 길다. 그리고 코드는 Money 의 코드와 똑같다.

1
2
3
4
5
//Sum
@Override
public Expression plus(Expression addend) {
return new Sum(this, addend);
}

일단, Sum.times() 가 작동하게 된다면, Expression.times() 를 선언하는 일은 쉽다. 테스트는,

1
2
3
4
5
6
7
8
9
10
@Test
void testSumTimes(){
Expression fiveBucks = Money.dollar(5);
Expression tenFrancs = Money.franc(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression sum = new Sum(fiveBucks, tenFrancs).times(2);
Money result = bank.reduce(sum, "USD");
assertEquals(Money.dollar(20), result);
}
1
2
3
4
5
6
//Sum
Expression times(int multiplier) {
return new Sum(
augend.times(multiplier), addend.times(multiplier)
);
}

이제 컴파일되게 만들려면, Expression 에 times() 를 선언해야한다.

1
2
3
4
5
6
7
interface Expression {
Money reduce(Bank bank, String to);

Expression plus(Expression addend);

Expression times(int multiplier);
}

위 작업 때문에, Money.times() 와 Sum.times() 의 가시성을 높여야한다.

Read more