LostCatBox

TDD by Example

Word count: 1.6kReading time: 10 min
2025/03/10 15 Share

참고

왜?

  • 테스트 주도 개발을 통해 불안전한 코드 작성을 회피하고, 좀더 짜임세 있는 구조, 테스트를 잘하기 위한 구조로 설계 및 개발하기 위해

TDD 방법론

TDD-> 원하는 테스트 작성후 실패(레드) -> 성공시키기(그린) -> 중복제거 및 구조변경(리팩토링)

  1. 테스트를 작성한다(마음속에 있는 원하는 오퍼레이션이 코드로 어떤식으로 나타나길 원하는지 생각하고, 이야기를 쓴다. 원하는 인터페이스를 개발하라,올바른 답을 얻기위한 필요한 이야기의 모든 요소를 포함시켜라)
  2. 실행 가능하게 만단다.(2가지 방법이있다. 1. 가짜 구현 = 상수를 반환하게 만든다. 추후에는 변수로 치환한다.실행가능하게 만들기만하면된다. 2. 진짜구현 = 간단한 수정사항 구현가능하다면 진행
  3. 올바르게 만든다. 이제 시스템이 동작(테스트통과) 하므로 직전에 저질렀던 죄악을 수습하자. 좁고 올곧은 소프트웨어 정의의 길로 되돌아와서 중복을 제거하고 초록색으로 만들자
  4. 결함을 찾거나 요구사항늘어날시 할일추가 작성 다음 할일 선택후 1번 테스트작성

TDD에서 꿀팁 3가지

  1. 테스트를 확실하게 돌게하는 3가지
    1. 가짜 구현하기
    2. 진짜 구현하기
    3. 삼각측량법
  2. 테스트 주도 설계방법
    1. 테스트 코드와 비즈니스 로직 코드사이에 중복을 찾고, 제거 및 리팩토링 하기
    2. 메타포(비유된 개념을 인터페이스?객체?로 구현)를 이용하여 설계가 더 쉽다.
  3. 테스트가 어려운 부분과 쉬운 부분에 따라 속도 조절하기

테스트 주도 개발 패턴

격리된 테스트

  • 응집도는 높고 결합도는 낮게 다른 테스트에는 영향도가 없도록 격리되도록 설계해야한다.

테스트 목록

  • 테스트 전 반드시 테스트 목록을 작성해놓는다.
  • 구현해야할 모든 오퍼레이션 작성, 현재 존재하지않는 오퍼레이션 경우 null 버전으로작성
  • 초록 막대후에 추가적으로 테스트 필요시 추가한다.
    • 제대로 작동하지 않는 테스트를 하나라도 생각할수있다면, 릴리즈보다 중요하다.

단언 우선

  • 완료된 시스템, 기능, 메서드 등을 먼저 적어놓고 시작하자.

빨간 막대 패턴

한 단계 테스트

  • 뻔한 테스트 및 구현 말고, 잘모르겠지만 일단 구현할수있겟다? 정도 레벨부터 시작하는것이 좋다.

테스팅 패턴

자식 테스트

  • 지나치게 큰 테스트라면, 작은 테스트 케이스를 작성하여 실행되도록하라

모의 객체

  • 비용이 많이들거나, 리소스에 의존하는 객체를 테스트할경우, 모의 객체(MOCK)사용

크래시 테스트 더미

  • 호출되지 않을 것 같은 에러 코드를 어떻게 테스트하나? 특수한 객체를 만들어 이를 호출한다.
    • 테스트 대상 로직이 정상통과해도 마지막에 fail() 을 통해 Exception 그리고 테스트 대상 객체 생성시 원하는 에러코드 반환하도록 오버라이딩한 테스트 진행.

깨진 테스트 체크인

  • 혼자 개발을 하는 경우, 깨진 테스트 체크인을 하므로서 다음날이 되어도 무엇을 해결해야하는지 정확히알수있다.

초록 막대 패턴

가짜로 구현하기

삼각 측량법

진짜 구현하기

디자인 패턴

커맨드

값 객체

널 객체

템플릿 메서드

플러거블 객체

플러거블 셀렉터

팩토리 메서드

임포스터

컴포지트

수집 매개 변수

리팩토링

차이점 일치시키기

데이터 이주시키기

하나를 여러개로 전환시키기

메서드 인라인

인터페이스 추출하기

TDD 책을 읽으며 나왔던 코드 전체

TestCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155


// TDD-> 원하는 테스트 작성후 실패(레드) -> 성공시키기(그린) -> 중복제거 및 구조변경(리팩토링)
// 1.테스트를 작성한다(마음속에 있는 원하는 오퍼레이션이 코드로 어떤식으로 나타나길 원하는지 생각하고, 이야기를 쓴다. 원하는 인터페이스를 개발하라,올바른 답을 얻기위한 필요한 이야기의 모든 요소를 포함시켜라)
// 2. 실행 가능하게 만단다.(2가지 방법이있다. 1. 가짜 구현 = 상수를 반환하게 만든다. 추후에는 변수로 치환한다.실행가능하게 만들기만하면된다. 2. 진짜구현 = 간단한 수정사항 구현가능하다면 진행
// 3. 올바르게 만든다. 이제 시스템이 동작(테스트통과) 하므로 직전에 저질렀던 죄악을 수습하자. 좁고 올곧은 소프트웨어 정의의 길로 되돌아와서 중복을 제거하고 초록색으로 만들자
// 4. 결함을 찾거나 요구사항늘어날시 할일추가 작성 다음 할일 선택후 1번 테스트작성

public class MoneyTest {
// 기존 = 화폐단위 하나에 대해서만 각 주식수*주당가격을 합계 프로그램이 개발되어있었다.
// 개발 목적 = 다양한 화폐로, 구성된 주식수*주당가격을 최종 합산하는 프로그램을 개발해야한다.
// 예시 = 10dollar*1 + 10CHF*1 = 15dollar (dollar/CHF= 0.5)
// 할일
// 다른 화폐단위 합 지원 -> 10dollar + 10CHF = 15dollar (dollar/CHF= 0.5)
// 곱하기 지원 -> 10dollar*2 = 20dollar

//곱하기 지원이 쉬워보이니까 먼저 지원해보자
//Money는 10
// money원하는 특성은 5
//현재 만드는 것의 특성은 ID값이 없는 값 객체의 특성을 알게 됨 -> 이는 equals()와 hashCode()재정의가 필요함을 알게됨
@Test
public void test1Multipl(){
//팩토리 메서드 도입이유는, 테스트에서 하위클래스에서 상위클래스 의존성을 줄일수있으므로
Money dollar10 = Money.dollar(10);
Expression dollar20 =dollar10.times(2);
assertEquals(Money.dollar(20), dollar20);
Expression dollar30 =dollar10.times(3);
assertEquals(Money.dollar(30), dollar30);
}
//현재 만드는 것의 특성은 ID값이 없는 값 객체의 특성을 알게 됨 -> 이는 equals()와 hashCode()재정의가 필요함을 알게됨
// Money 상위 클래스로 개발후에 Franc와 Dollar를 비교하는 코드가 필요하다는것을 알고 계속 할일추가 및 테스트작성
// Money 상위 클래스로 equals를 통합했는데, 중간에 assertFalse(Money.dollar(10).equals(Money.franc(10))); 해당부분실패. 즉, Money equals에는 getClass보다 이제 currency 동일성 비교가 필요해보임
@Test
public void test1equality(){
assertTrue(Money.dollar(10).equals(Money.dollar(10)));
assertFalse(Money.dollar(10).equals(Money.dollar(11)));
assertTrue(Money.franc(10).equals(Money.franc(10)));
assertFalse(Money.franc(10).equals(Money.franc(11)));
assertFalse(Money.dollar(10).equals(Money.franc(10)));
assertFalse(Money.franc(10).equals(Money.dollar(10)));
}

//이제 곱하기 기능은 지원이 가능해졌다. 그럼 "다른 화폐단위 합 지원" 해보자
//먼저 화폐단위를 추가해보자 Franc추가
//중복 코드가 매우 많아졌고, Money라는 상위 클래스로 모으기로함
@Test
public void test1FrancMultipl(){
Money franc10 = Money.franc(10);
Expression franc20 =franc10.times(2);
assertEquals(Money.franc(20), franc20);
Expression franc30 =franc10.times(3);
assertEquals(Money.franc(30), franc30);
}

//통화 개념을 넣기위해 currency 등장. 어떻게 테스트 하고 활용하기를 원하는가>
//assertEquals("USD", Money.dollar(1).currency); 을 원한다.
@Test
public void testCurrency(){
assertEquals("USD", Money.dollar(1).currency());
assertEquals("CHF", Money.franc(1).currency());
}

// @Test
// public void testDifferentClassEquality(){
// //해당부분이 성공해야한다. currency에대한 변수값이 중요함으로
// assertTrue(new Money(10, "CHF").equals(new Franc(10, "CHF")));
// }

//합산에 대한테스트 작성
//10dollar + 10CHF = 15dollar (dollar/CHF= 0.5)
//화폐단위를 치면, 나중에 값을 반환해주는것이필요할거같은데? plus 함수만들어서 같은거면 가능 아니면 valid
//다른 환율은 어떻게 계산하지?
//계산하는 동작을 객체로 만들어 저장한다면 어떨까? 계산을 Expression으로 메타포로 표현하면 어떨까?
@Test
public void testSum(){
// Money dollar = Money.dollar(10).plus(Money.dollar(10));
// assertTrue(dollar.equals(Money.dollar(20)));
// Money franc = Money.franc(10).plus(Money.franc(10));
// assertTrue(franc.equals(Money.franc(20)));
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Expression reduced = bank.reduce(sum, "USD");
assertTrue(reduced.equals(Money.dollar(10)) );
}
@Test
public void testplusReturnSum(){
Money five = Money.dollar(5);
Expression expression = five.plus(five);
Sum sum =(Sum) expression;
Bank bank = new Bank();
Expression reduced = bank.reduce(sum, "USD");
assertTrue(reduced.equals(Money.dollar(10)) );
}
@Test
public void testReduceSum(){
Sum sum = new Sum(Money.dollar(10), Money.dollar(10));
Bank bank = new Bank();
Expression reduced = bank.reduce(sum, "USD");
assertTrue(reduced.equals(Money.dollar(20)) );
}
@Test
public void testReduceDollar(){
Bank bank = new Bank();
Expression reduced = bank.reduce(Money.dollar(1), "USD");
assertTrue(reduced.equals(Money.dollar(1)) );
}
@Test
public void testBankDifferentCurrency(){
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression reduced = bank.reduce(Money.franc(2), "USD");
assertTrue(reduced.equals(Money.dollar(1)) );
}

@Test
public void testIdentityRates(){
assertEquals(1, new Bank().rate("USD", "USD"));
}

// 10dollar*1 + 10CHF*1 = 15dollar (USD/CHF= 2)
@Test
public void testBankDifferentCurrencySum(){
Expression franc = Money.franc(10);
Expression dollar = Money.dollar(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression plus = franc.plus(dollar);
Expression reduced = bank.reduce(plus, "USD");
assertTrue(reduced.equals(Money.dollar(15)) );
}

@Test
public void testMoneySum(){
Expression franc = Money.franc(10);
Expression dollar = Money.dollar(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression sum = new Sum(franc, dollar).plus(dollar);
Expression reduced = bank.reduce(sum, "USD");
assertTrue(reduced.equals(Money.dollar(25)) );
}
@Test
public void testExpressionTimes(){
Expression franc = Money.franc(10);
Expression dollar = Money.dollar(10);
Bank bank = new Bank();
bank.addRate("CHF", "USD", 2);
Expression sum = new Sum(franc, dollar);
Expression times = sum.times(2);
Expression reduced = bank.reduce(times, "USD");
assertTrue(reduced.equals(Money.dollar(30)) );
}

}

Bank

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Bank {

private Hashtable rates = new Hashtable();
// 해당 reduce부분은 원래 SUM객체의 public 변수를 직접 접근하여 계산하고 있었으며,
// 이를 sum객체에 reduce 메서드를 만들어 외부에서는 해당 변수 접근을 안하며, private으로 변경하였다.
// 이후, 현재 reduce 를 만약 Expression 인터페이스에 적용한다면, 공통된 reduce를 갖음에 따라 인스턴스 검사가 필요없어진다.


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

public void addRate(String source, String to, int rate) {
rates.put(new Pair(source, to), rate);
}

public int rate(String source, String to) {
if(source.equals(to)){ return 1;}
return (int)rates.get(new Pair(source, to));

}

private class Pair {
private String from;
private String to;

Pair(String from, String to) {
this.from = from;
this.to = to;
}

@Override
public boolean equals(Object o) {
Pair pair = (Pair) o;
return Objects.equals(from, pair.from) && Objects.equals(to, pair.to);
}

@Override
public int hashCode() {
return 0;
}
}
}

Money

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Money implements Expression
{
protected int amount;
protected String currency;
public String currency(){
return currency;
}
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
@Override
public Expression times(int count) {
return new Money(count*amount, currency);
}

public static Money dollar(int i) {
return new Money(i, "USD");
}
public static Money franc(int i) {
return new Money(i, "CHF");
}

@Override
public Expression plus(Expression dollar) {
return new Sum(this,dollar);
}

public int getAmount() {
return amount;
}
public boolean equals(Object o) {

Money dollar = (Money) o;
//답은 맞았는데, 클래스가 다르다. -> 중요한건 클래스가 아니라 애초에 통화다.
return amount == dollar.getAmount() && dollar.currency.equals(currency);
}

public Expression plus(Money addend) {
return new Sum(this, addend);
}
@Override
public Money reduce(Bank bank, String to) {
int rate = bank.rate(this.currency, to);
return new Money(amount/rate, to);
}
}

Sum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Sum implements Expression{
private Expression augend;
private Expression addend;

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

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

@Override
public Expression times(int multiplier) {
augend = augend.times(multiplier);
addend = addend.times(multiplier);
return this;
}

@Override
public Expression plus(Expression dollar) {
return new Sum(this, dollar);
}
}

Expression

1
2
3
4
5
6
7
interface Expression{

Money reduce(Bank bank, String to);

Expression plus(Expression dollar);
Expression times(int count) ;
}
CATALOG
  1. 1. 참고
  2. 2. 왜?
  3. 3. TDD 방법론
  4. 4. TDD에서 꿀팁 3가지
  5. 5. 테스트 주도 개발 패턴
  6. 6. 빨간 막대 패턴
  7. 7. 테스팅 패턴
  8. 8. 초록 막대 패턴
  9. 9. 디자인 패턴
  10. 10. 리팩토링
  11. 11. TDD 책을 읽으며 나왔던 코드 전체