가. 핵심
1) 테스트에서 구현으로 이어지는 과정은?
•
실패하는 테스트 케이스를 작성한다
•
실패하는 테스트 케이스에서 컴파일 에러를 해결한다
•
테스트 케이스를 통과하는 구현 코드를 작성한다
•
구현 코드 내 중복 또는 구현 코드와 테스트 코드 내 중복을 제거하기 위해 리팩토링을 한다
2) 설계를 유기적으로 키워가는 과정은?
핵심 아이디어: 처음부터 완벽한 설계가 나오는 것은 불가능하다
•
실패하는 테스트를 작성한 후, 성공한는 테스트를 만들기 위해 인터페이스를 수정한다
•
인터페이스 수정에 따라 테스트 케이스의 목표는 유지하면서 테스트 코드의 세부사항을 수정한다
•
테스트 코드를 통과시키기 위해 구현 코드를 수정한다
노하우: 설계를 키우는 과정에서 구현 코드를 추상화한다
•
설계를 키워나가기 위해서는 리팩토링 과정에서 하위 클래스의 공통 책임을 찾아서 상위 클래스 혹인 인터페이스로 전가해야 한다
•
책임을 상위로 전가하는 과정에서 모든 하위 클래스가 더이상 독자적인 구현부를 갖지 못하다면 하위 클래스를 모두 제거하는 방향으로 객체의 추상화를 이뤄낼 수 있다
나. 주요개념
1) 큰 테스트를 작은 테스트로 쪼개는 과정은?
•
핵심 테스트를 달성하기 위해 기본적으로 달성되어야할 과정에 대해 ‘할 일 목록'으로 정리한다
→ 쪼개어져 작아진 테스트 케이스 중에서 가장 쉬운 것 또는 가장 핵심적인 내용을 다루는 테스트 케이스를 우선 순위로 선택한다
→ 예시
- 큰 테스트 케이스: 5$ + 5₩ == 9$
- 작은 테스트 케이스 1: 5$ + 5$ == 10$
- 작은 테스트 케이스 2: 5$ == 5$
- 작은 테스트 케이스 3: 5$ * 2 == 10$
- 작은 테스트 케이스 4: 5₩+ 5₩ == 10₩
- 작은 테스트 케이스 4: 5₩ == 5₩
- 작은 테스트 케이스 4: 5₩ * 2 == 10₩
Plain Text
복사
•
작은 테스트로 쪼개는 과정에서 객체에 대한 구현은 신경 끄고 오로지 작은 테스트 케이스를 떠올리는 것을 우선한다
•
작은 테스트 케이스가 나열되었다면, 테스트 케이스를 통과하기 위해 필요한 객체를 나열하고 객체간의 협력 관계에 대해 고민한다
2) 테스트 코드와 구현 코드 사이의 가장 큰 문제와 해결방법은?
•
의존성, 구현 코드의 일부를 수정하면 이어서 테스트 코드의 일부도 수정해야 하는 코드 간 의존관계가 가장 큰 문제다
•
해결방법은 구현 코드와 테스트 코드 사이에 의존되어 있는 코드중복을 제거하는 것
•
코드중복을 제거하는 구체적인 방법은 객체를 생성하여 해당 코드중복을 객체로 넘기는 것
•
다음 테스트 코드로 넘어가기 전에 전 테스트 코드에 남겨진 중복은 위의 방법을 통해 반드시 제거해야 한다
•
예시
class Money(){
private int amount;
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
}
}
public Fran extends Money(){
private int amount;
public boolean equals(Object object) {
Money fran = (Fran) object;
return amount == fran.amount
}
}
// to
class Money(){
protected int amount;
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
}
}
// Fran
public Fran extends Money(){
public boolean equals(Object object) {
Money money = (Money) object;
return amount == money.amount
}
}
// to
// Fran 클래스 제거
Java
복사
•
더불어 테스트 코드에서는 상위 클래스를 주로 명시하고 하위 클래스에 대한 직접 참조를 줄임으로써 구현부의 변화와 관계없이 테스트 코드의 변화를 최소화할 수 있다
3) 테스트를 우선 통과시키기 위해 켄트 백이 자주 사용하는 방법은?
주로 가짜 구현과 명백한 구현을 번갈아가며 사용, 삼각측량은 유기적 설계 방법이 떠오르지 않을 때 선택적으로 사용
•
가짜 구현: 스텁 구현(stub implementation) 또는 메소드의 반환값에 상수 대입을 통해서 오로지 테스트 통과만을 목적으로 구현한다
→ 스텁 구현의 목적은 컴파일 에러에서 벗어나는 것
→ 상수 대입의 목적은 테스트 케이스를 통과하는 것
// 스텁 구현
Dallor times(int muliplier) {
amount *= multiplier;
return null
}
// 상수 대입
Dallor times(int muliplier) {
amount *= 2;
return amount
}
Java
복사
•
명백한 구현: 테스트 통과를 위해 어떻게 구현하면 될 지에 대해 머릿속에 구체적인 로직이 잘 그려질 경우 그대로 구현한다
•
삼각측량: 테스트 통과를 위한 구현 코드의 설계가 머릿속에 떠오르지 않을 경우, 동일한 목표에 대한 두 개 이상의 테스트 케이스를 작성해서 통과할 수 있는 방법을 추측해본다. 귀납적 추론으로
→ 상수 대입만으로 하나의 테스트 케이스는 통과했으나 명백한 구현으로 갈 수 없는 경우에 삼각측랴을 시도해보면 좋다
// 통화 간 합계에 대한 테스트 케이스를 두 개 이상 작성한다
// 테스트 케이스 사이의 연관성을 찾아서 구현에 대한 설계적 아이디어를 얻는다
테스트 케이스 1: 5$ + 5₩ == 9$
테스트 케이스 2: 1$ + 10₩ == 9$
테스트 케이스 3: 10$ + 1₩ == 10.8$
Plain Text
복사
4) 리팩토링을 하던 중 새롭게 테스트 해야 할 요소를 만난 경우 대체방법은?
•
새롭게 테스트 해야 할 대상이 리팩토링하고 있는 요소를 해결하기 위한 근본적인 대상이라면 리팩토링하던 부분을 롤백하고 새롭게 테스트할 요소에 대해 테스트 케이스를 작성한다
5) 리팩토링 시 구현코드를 수정할 때 켄트백이 제시한 방법은?
•
매개변수로 전달된 상위 클래스에 대해 특정 하위 클래스 여부를 검사하는 코드는 다형성을 이용할 것
→ 클래스 여부 검사에 따른 구현 부분(ex reduce)을 상위 클래스나 인터페이스에 정의
•
예시
// by defining reduce in Expression, Money, Sum objects
//Expression
Money reduce(String to)
// Money
public Money reduce(String to) {
return this;
}
// Sum
public Money reduce(String to) {
int amount = augend.amount + addend.amount;
return new Money(amount, to);
}
Java
복사
// Bank
Money reduce(Expression source, String to) {
if (source instanceof Money) {
return (Money) source.reduce(to);
}
Sum sum = (Sum) source;
return sum.reduce(to);
}
// to
Money reduce(Expression source, String to) {
return source.reduce(to)
}
Java
복사
Reference
•
Kent Beck, Test-Driven Development: By Example