- 개발 환경은 iOS/Swift 입니다.
- 각 챕터별로
feature/chX브랜치를 만들어 실습을 진행합니다.- 예:
feature/ch3,feature/ch4, ...
- 예:
- 실습이 끝나면
main브랜치에 머지하여 항상 최신 상태를 유지합니다. - 따라서
main브랜치에는 모든 챕터의 누적된 결과물이 반영되어 있습니다. - 필요하다면 특정 챕터의 브랜치로 이동하여 해당 시점의 실습 코드를 확인할 수 있습니다.
- 테스트와 구현 단계를 더 작은 단위로 나눌 수 있음을 깨달았다.
- 중복 제거를 목표로 코드를 지속적으로 개선했다.
- p144까지 진행하면서 “어떻게 이런 구조가 만들어졌지?” 하는 신기함을 느꼈다.
- 테스트가 하위 클래스에 의존하지 않고 상위 타입에 의존하도록 변경해도 동일하게 성공하도록 만들었다.
- 이를 통해 어떤 모델 코드에도 영향을 주지 않고 상속 구조를 자유롭게 변경할 수 있게 되었다.
- 하위 클래스를 제거하기 위해 두 클래스의
currency구현을 동일하게 리팩터링했다. - “불안하면 보폭을 좁히고, 답답하면 보폭을 넓혀라.” — TDD 과정 전반에서 지속적으로 적용해야 할 조율임을 배웠다.
- 중복된 부분을 호출자(팩터리 메서드)로 옮겨 두 생성자를 일치시켰다.
- 하위 클래스에서 상위 클래스로 메서드를 올리기 전에, 먼저 모든 하위 클래스의 코드가 동일해지도록 리팩터링했다.
- 구조가 바뀌면서 불필요해진 테스트는 리팩터링 과정에서 제거했다.
- 설계 방향이 명확하지 않을 때는 가짜 구현으로 시작하고, 이후 리팩터링하는 방식으로 접근했다.
- 큰 테스트를 작은 테스트로 나누어 진행했다.
- 예:
$5 + 10CHF→$5 + $5
- 예:
- 모든 중복이 제거되기 전까지는 테스트를 완성된 것으로 보지 않았다.
- 라이브러리 연산에 대한 가정을 테스트 코드로 작성해 직접 검증했다.
- 테스트 코드의 반환 타입을 상위 타입으로 바꾸고, 발생하는 컴파일 에러를 해결하면서 해당 멤버를 상위 타입으로 옮기는 방식으로 리팩터링했다.
- TDD로 구현할 경우 테스트 코드의 줄 수와 모델 코드의 줄 수가 거의 비슷해진다.
- TDD가 경제적이려면 매일 만들어내는 코드의 양이 두 배가 되거나, 동일한 기능을 절반의 코드로 구현해야 한다.
- 또한 TDD와 기존 방식의 차이를 측정할 때는 디버깅, 통합, 설명에 걸리는 시간까지 포함해야 한다는 점을 깨달았다.
$5 + $5연산에서Money를 반환하는지 실험하기 위한 테스트 추가
public void testPlusSameCurrencyReturnsMoney() {
Expression sum = Money.dollar(1).plus(Money.dollar(1));
assertTrue(sum instanceof Money); // Sum이 아닌 Money가 반환되길 기대
}- 책의 번역에서는 이 테스트를 통과시키기 위한 코드 예시가 모호했으나, 실제 의도는 아래와 같았다.
public Expression plus(Expression addend) {
return new Sum(this, addend);
}- 하지만 인자가
Money일 경우, 그 통화가 동일한지 확인하는 분명하고도 깔끔한 방법은 제시되지 않았다. - 만약 설명대로 구현하면 다음과 같은 코드가 되지만, 이는 깔끔한 방법이라고 보기 어렵다.
public Expression plus(Expression addend) {
if (addend instanceof Money && this.currency.equals(((Money)addend).currency)) {
return new Money(this.amount + ((Money)addend).amount, currency);
}
return new Sum(this, addend);
}코드와 테스트의 함수 수와 줄 수가 비슷하다는 점을 확인했다.
- 프로그램의 제어 흐름에서 독립적인 경로의 수를 측정하는 지표.
if,switch,for,while,and,or등이 많을수록 복잡도가 커진다.
- 테스트 품질을 충분히 측정하기는 어렵지만 출발점이 될 수 있다.
- TDD를 철저히 따르면 100% 문장 커버리지를 달성할 수 있어야 한다.
- JProbe(www.sitraka.com/software/jprobe) 결과:
- 단 하나의 메서드
Money.toString()만 커버되지 않았음. - 이는 실제 모델 코드가 아니라 디버깅을 돕기 위해 추가된 코드였다.
- 단 하나의 메서드
- 아이디어: 코드 한 줄의 의미를 바꾸면, 반드시 테스트가 실패해야 한다.
- 수동으로 하거나 Jester 같은 도구를 사용할 수 있다.
- Jester 결과:
Pair.hashCode()의 한 줄을 0으로 반환하도록 속여도 테스트가 실패하지 않음.- 하지만 0 대신 다른 상수를 반환하는 것은 프로그램 의미를 바꾸지 않는다는 점에서 실제 결함으로 보긴 어렵다.
- 테스트 커버리지를 높이는 또 다른 방법은 테스트 수를 늘리는 것이 아니라, 프로그램 로직을 단순화하는 것이다.
“… 입력의 모든 경우의 수를 따라가며 커버리지를 늘리기보다는,
코드를 단순화함으로써 동일한 테스트들이 더 많은 경우를 자연스럽게 커버하게 만든다.”
bootstrap
맨바닥에서부터 스스로 만들어 올리다.
- p172 — 한 번에 메서드 하나 이상 수정하지 않으면서 테스트를 통과시키는 방법을 찾도록 노력한다.
- 외부 자원을
setUp에서 할당했다면, 반드시tearDown에서 반환해야 한다. - 호출된 메서드의 로그를 남기는 전략으로 테스트를 강화할 수 있다.
- 항상 로그의 끝에 기록을 추가하면 메서드 호출 순서를 쉽게 알 수 있다.
- 다음에 구현할 테스트는
- 나에게 가르침을 줄 수 있는 것
- 내가 만들 수 있다는 확신이 드는 것
- 새로운 테스트에서 문제가 생기면, 두 단계 뒤로 물러서서 더 단순한 테스트를 시도한다.
- 실패한 테스트를 발견하면, 더 세밀한 단위 테스트를 작성해 올바른 출력을 확인한다.
- 중복은 언제나 나쁘다. (놓친 디자인 요소를 찾기 위해 일부러 만드는 경우 제외)
TestSuite를 Composite Pattern으로 구현 → 테스트 하나와 집합을 동일하게 다룬다.
- 새로운 언어를 접하면 그 언어로 xUnit을 직접 만들어본다.
- 보통 테스트 8~10개를 통과할 즈음이면 그 언어의 주요 기능을 대부분 경험하게 된다.
🔥 이해하기 어려웠던 문장 (p195, 원문)
“There is substantial duplication here, which we could eliminate
if we had a way of constructing a suite automatically given a test class.”
➡️ 번역:
“여기에는 상당한 중복이 있다.
만약 테스트 클래스를 주면 자동으로 수트를 구성할 수 있는 방법이 있다면, 그 중복을 없앨 수 있을 것이다.”
보다 정확한 이해를 위해 원서로 학습하고 내용을 정리하고 있습니다.