Search
Duplicate
📘

Test-Driven Development - Kent Beck

가. 핵심

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