17. 17
Test-driven development?
However..
› 누구나 겪어봤을 고통..
1. 설계: 불행하게도 초반에 완벽한 설계를 하기는 힘들다.
2. 코딩: 설계가 완벽하지 않기에 코딩이 산으로 간다.
3. 테스트: 설계<->코딩 주기를 반복하며, 요구사항이 어느
정도 구현되고 나서야 작성되기 시작한다.
4. 요구사항변경 or 설계미스발견: 테스트 코드작성은
아직 미완상태. 이제 모든것이 꼬이기 시작한다..
5. 리펙토링: 포기.
20. 20
Test-driven development?
So we need courage!
› 용기를 얻기 위해선 어떻게 해야할까.
1. 불확실한 상태로 있는 대신, 가능하면 재빨리 구체적인 학습을 시작
한다.
2. 침묵을 지키는 대신 좀 더 분명하게 커뮤니케이션을 한다.
3. 피드백을 회피하지 말고 도움이 되는 구체적인 피드백을 찾는다.
26. 26
By Example
Scenario
› 회사가 판매하는 채권 포트폴리오 관리 시스템의 새로운 고
객이 문득 이런 요구사항을 던진다.
"시스템이 제공하는 기능들에 굉장히 감탄했습니다. 헌데, 미
합중국 달러로 명명된 채권만 다루고 있더군요.
저는 새로운 채권 펀드를 시작하려고 하는데 제 전략상 다른
화폐로 채권을 다룰 필요가 있습니다.“
29. 29
By Example
System implementation
› 테스트 코드를 작성해본다.
› 컴파일 실패!
1. 아직 Dollar 클래스가 존재하지 않는다.
2. Dollar 내부엔 int를 입력받는 생성자가 필요할 것이다.
3. Dollar 내부엔 times(int) 라는 함수가 필요할 것이다.
37. 37
By Example
System implementation
› 시스템이 앞으로 제공해야 할 기능은 다음과 같을 것이다.
종목 주 가격 합계
IBM 1000 25USD 25000USD
Novartis 400 150CHF 60000CHF
합계 65000USD
※ 환율 : 1.5CHF = 1USD
38. 38
By Example
System implementation
› To-Do List를 갱신한다.
To-Do List
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5USD = 10USD
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
39. 39
By Example
System implementation
› 테스트 코드를 작성해본다.
› 컴파일 실패!
1. 아직 Franc 클래스가 존재하지 않는다.
2. Franc 내부엔 int를 입력받는 생성자가 필요할 것이다.
3. Franc 내부엔 times(int) 라는 함수가 필요할 것이다.
41. 41
By Example
System implementation
› 리펙토링 단계
› Dollar클래스와 Franc 클래스 사이의 중복들이 거슬린다.
› 둘을 묶을 수 있는 상위 클래스가 있다면 어떨까?
› To-Do List는 항상 열려있다.
To-Do List
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5USD = 10USD
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
- Dollar와 Franc 클래스의 상위 클래스인 Money 클래스
49. 49
By Example
System implementation
› To-Do List 갱신
› Dollar와 Franc의 amount가 같더라도 둘의 가치는 다르다.
› Dollar와 Franc 클래스를 비교하는 기능이 필요하다.
To-Do List
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5USD = 10USD
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
- Dollar와 Franc 클래스의 상위 클래스인 Money 클래스
- Dollar와 Franc 클래스의 비교 : Dollar(5) != Franc(5)
53. 53
By Example
System implementation
› 테스트를 통과하려면? (전략수립)
1. Dollar와 Franc에서 각각의 통화가 무엇인지 기억한다.
2. 객체간 비교시, 각 개체의 통화정보를 기준으로 USD로 변
경한 후 비교를 수행한다.
3. 환율정보를 갖고 있는 ‘무언가’가 필요하다.
4. ‘무언가’의 이름으로는 Bank가 적절할 것 같다.
60. 60
By Example
System implementation
› 또 다시 테스트 실패
› 테스트 케이스에서 환율에 대한 정보를 입력하지 않았다.
› bank.addRate(“USD”, 1.0); //추가
› bank.addRate(“CHF”, 1.0); //추가
62. 62
By Example
System implementation
› To-Do List 갱신
› 드디어 서로 다른 통화간의 합이 남았다
To-Do List
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5USD = 10USD
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
- Dollar와 Franc 클래스의 상위 클래스인 Money 클래스
- Dollar와 Franc 클래스의 비교 : Dollar(5) != Franc(5)
66. 66
By Example
System implementation
› To-Do List 갱신
To-Do List
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5USD = 10USD
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
- Dollar와 Franc 클래스의 상위 클래스인 Money 클래스
- Dollar와 Franc 클래스의 비교 : Dollar(5) != Franc(5)
70. 70
TDD basic strategy
Independent test
› 각 테스트는 독립성이 보장되어야 한다.
› 테스트는 충분히 빨라야한다.
Q. 테스트의 독립성을 위해 Test마다 시스템을 재가동 한다면
?
Q. 재빠른 테스트를 위해 한 가지 Test 내에서 두 가지 이상의
기능을 검증한다면?
› 잘만 지키면, 설계 응집도는 상승하고 결합도는 낮아진다.
71. 71
TDD basic strategy
To-do list
› 적는 습관을 들인다!
› TO-DO LIST는 카테고리 별로 잘 정리한다!
-> 규모, 우선순위 등등
› 머릿속에만 의지하게 되면..
1. 경험이 축적될수록 TO-DO LIST가 많아진다.
2. LIST가 많을 수록 집중도가 떨어진다.
3. 집중도가 떨어지면 실수가 발생한다.
72. 72
TDD basic strategy
Bottom-Up? Top-down?
› 간단한 한가지 사례를 나타내는 테스트에서 시작했다면?
-> Top-Down 식 구현.
› 전체의 작은 한 조각에 해당하는 테스트에서 시작했다면?
-> Bottom-UP 식 구현.
› 즉, TDD는 상향 또는 하향에 종속된 개념이 아니다.
‘Known-to-unknown’
74. 74
Testing pattern
Mock object
› 서버에 대한 테스트가 필요한데, 클라이언트가 없을 경우,
› 원격서버와의 통신중 물리환경에 의해 테스트가 실패될 때,
› 테스트 중 상대 객체에 대한 호출내역이 궁금할 때,
모의객체 (Mock Object)로 원하는 상황을 연출가능!
75. 75
Testing pattern
Crash test dummy
› 에러 상황에 대해 테스트할 때 사용
› 객체 전체를 흉내내지 않는 점을 제외하면 Mock Object와 유
사.
› EX)
파일 시스템에 여유공간이 없는 상황을 연출하고 싶을 때.
76. 76
Testing pattern
Crash test dummy
Private class FullFile extends File {
public FullFile(String path) {
super(path);
}
public boolean createNewFile() throws IOException {
throw new IOException();
}
}
77. 77
Testing pattern
Crash test dummy
public void testFileSystemError(){
File f = new FullFile(“foo”);
try{
saveAs(f);
fail(); // 예외처리에 실패하면 테스트가 종료
} catch(IOException e) {
// Do Something
}
}
79. 79
Design patterns
in junit
xunit?
› xUnit 은 스몰토크4의 테스트 프레임워크 SUnit 의 구조와 함
수들을 따라한 유닛 테스팅 프레임워크들을 총칭.
› 첫 글자는 사용하는 언어에서 따온다.
› 즉, JUnit은 자바를 위한 테스트 프레임워크다.
81. 81
Retrospective
Through TDD?
› 짧은 사이클의 반복으로 설계 방향을 누차 재점검 할 수 있다.
변화가 빨리 도입되는 극한적 상황에서 유리하다.
› 커뮤니케이션의 용이해진다. 테스트 코드는 곧 작업에 대한
history이기도 하다.
› 한참 뒤에 들이닥치는 변동사항에도 끄떡없다.
83. 83
My & your assignment
What else?
› 실제로 해보기! 많이!
› 읽어보기: TDD is dead long live testing by DHH
› 시청하기: 유닛테스트의 효용성 by 김포프
› TDD에 대한 ‘나’의 입장 정리해보기.
84. 84
[1] Test Driven Development: By Examples, Kent Beck
[2] 테스트 주도 개발 (Test-Driven Development), 박상준
[3] 테스트 자동화와 TDD, 박경훈
[4] JUnit A Cook's Tour
[5] Java Reflection API, stackoverflow
[6] Design Patterns, Programcreek
[7] Extreme Programming, Wikipedia
[8] TDD는 죽었다, DHH, 이상욱 역
[9] C++ 유닛테스트, 김포프
references
Editor's Notes
중요한 것은 TDD를 하는 방법이 아닌 TDD의 철학을 음미하는 것.
사상을 이해하지 않고서는 올바른 사용을 할 수가 없다.
어떻게?
How를 통해 Why로 접근해갈 것이다.
배우고, 아는 것을 행하며, 실천함으로 체득하자.
체득은 또 다시 새로운 실천으로 이어질 수 있도록 해야하겠다.
동사 테스트: 버튼을 몇 개 눌러보고 화면에 나오는 결과를 주시하는 그런 모습이 떠오르지 않는가?
명사 테스트: 자동화된 테스트는 수동 테스트와 달리 ‘소유’할 수 있다.
시간낭비 돈낭비
삶의 질 하락..
이런저런 변경이 자연스레 발생하기 때문.
잘못 건들면 터진다. 리펙토링하자고 버그만드는 경우.
아까 도입부에서 “테스트 / 주도개발” 중 ‘테스트’에 먼저 접근해 보자고 했다.
아직 한 발 남았다.
잠시 다른 화제로 전환.
비교를 위해 일반적인 개발 방식인 Architecture Driven Development 방식을 살펴보자.
허나 잘 생각해보면 여기서도 자동화된 테스트코드 작성은 가능하다.
즉 자동화된 테스트는 TDD의 전유물이 아니다.
스트레스를 많이 받으면 -> 테스트를 뜸하게 하게된다.
테스트가 뜸해지면 -> 에러 생산률이 올라간다.
에러가 많아지면 -> 스트레스가 커진다.
악순환의 반복..
아까의 다이어그램과 ‘대우’ 관계!
테스트우선 -> 불확실성을 줄이면서 스트레스를 감소시킴
스트레스 감소 -> 테스트 작성에 긍정적 효과
자 이로써 Test-Driven Development의 이름풀이가 끝났다.
to-do list로부터 적절하게 작은 크기의 일을 골라 테스트를 작성한다.
돌려보면 당연히 실패할 것이다. 아직 구현된게 없으니까. 테스트 실패상태를 TDD에선 주로 Red Bar 라고 한다. (빨간막대)
최대한 빠르고 비열한 방법으로 테스트를 통과시킨다.
비록 떳떳하진 않지만 어쨌건 테스트는 통과한다. 테스트 성공상태를 TDD에선 주로 Green Bar라고 한다. (초록막대)
리펙토링을 통해 “중복을 제거”한다.
단일사고적(Monological) 접근은 TDD의 미덕이다.
위 단계를 통해 “Clean code that works” 에 단일사고적으로 접근한다.
3번에서 “Code that works”를 만들고
5번에서 “Clean code”를 만드는 것.
악몽과도 같은 상황..
참고로 테스트에 사용된 Test Framework는 gtest-1.7.0이다.
생성자 내부에서 아무것도 안한다던지
Times가 상수를 반환한다던지..
엉망인 코드다.
하지만 지금 목표는 빨리 테스트를 통과시키는 것이니 이는 무시하자.
- USD와 CHF를 더해서 USD로 나타내는 기능 : 10USD + 15CHF = 20USD
- 가격과 주를 곱해서 총 합계를 구하는 기능 : 2 * 5Franc = 10Franc
다시 사이클의 첫 번째 단계로 돌아간다.
헌데, 내부 구조만 변경할 것으므로 별도의 테스트 코드는 필요없을 듯 싶다.
테스트가 유지되므로 step 2도 넘어간다. (추가되는 코드가 없다. 테스트가 실패할 일이 없음)
그럼 Money를 구현할 차례다.
일단 Franc
공교롭게도 bank에 USD와 CHF 사이의 환율을 입력해주는 라인이 빠져있다.
테스트 초기화시 입력을 해주기 때문.
addRate() 라는 함수로 테스트 시작 전, CHF의 USD기준 환율 정보가 입력되고 있다.
사실 이번 스텝은 ‘비열한 방법으로 테스트 통과하기’ 이지만,
생각하는 구현에 충분한 확신이 있다면 구현 복잡도를 좀 더 올리는 것도 괜찮다.
단, 확신이 100%에 수렴하지 않으면 추천하지 않는다.
개발 주기를 자칫 루즈하게 만들고 스트레스 상황을 증가시킬 수 있기 때문.
불행중 다행으로 Bank 내부에 USD기준의 USD환율 정보가 없는 문제 하나만 발생
이번엔 한번에 많은 양의 코드를 작업했는데, 테스트에서 수도없이 런타임 에러가 발생했다면 멘탈붕괴가 왔을 것이다.
그런데 이거 아무리봐도 실패하는 테스트다.
Money에는 저런 형태의 생성자가 없다.
JWTest에 대한 간단한 소개
컴퓨터로는 코딩만. 필기는 칠판이용. -> 시연은 살아있어야 한다.
3. Progress
(1) 요구사항 정의. (2)~(5)에 대한 것
(2) fib(0)일때 0인지 확인
(3) fib(1)일때 1인지 확인
(4) fib(int)의 중복제거! (0과 1에 대해)
(5) 2이상일땐 fib(num) == fib(num-1) + fib(num-2) 임을 검증
(6) fib()의 입력이 음수일때 예외처리. (테스트코드는 반복문 이용)
(7) 오버플로에 대한 대비. 몇 번째 자리수까지 가능해야 하나?
: 값이 특정 순서에서 이전값보다 작아지면?!
(8) 테스트로부터 유도된 피보나치 완성!
이렇게 되면 테스트 과정에서의 스트레스가 증가한다. 바람직하지 않음.
아는 것에서 점차 모르는 곳으로 이동하는 것.
어느정도의 지식과 경험으로 시작하지만, 그 과정에서 점차 새로운 것을 배우게 된다는 것.
TDD의 메타포가 지향하는 방향성은 바로 ‘아는 것에서 모르는 것으로’ 다.
그리고 추상화된 테스트를 점차 구체화 시켜간다.
테스트 코드 작성에 대한 테크닉
Mock object는 말 그대로 테스트 대상의 상대역에 대한 대역이다.
마치 클라이언트처럼 행동하는 Mock Object 작성
마찬가지로 DB처럼 행동하는 Mock Object 작성
Mock Object 내의 함수가 불러질 때마다 카운트하면 된다.
예전 서버 프로그래밍시 버퍼 오버플로우 테스트를 예로 들자.
파일 생성을 하면 묻지도 따지지도 않고 그냥 예외를 던진다.
파일 생성을 하면 묻지도 따지지도 않고 그냥 예외를 던진다.
Junit의 구조를 보면 실제 Framework 상에서, 이제까지 짚어본 TDD의 전략 및 패턴이 어떻게 구현되어있는지 감을 잡을 수 있다.
앞서 다룬 내용들이 Framework에는 어떻게 담겨있을까?
JUnit Architecture: http://junit.sourceforge.net/doc/cookstour/cookstour.htm/
Patterns: http://www.programcreek.com/java-design-patterns-in-stories/