combacsa-20100610-1

Download Report

Transcript combacsa-20100610-1

Combacsa’s SPARCS Web Seminar
SOFTWARE DEVELOPMENT #2:
TEST-DRIVEN DEVELOPMENT
Test-Driven Development
Test-Driven Development
 Agile Family (of course!)
 Test Code 작성으로부터
프로그램 개발이 진행되게 하는 방법론.
 Two major philosophy
 코딩을 시작하기에 앞서,
실패하는, 자동화된 테스트 코드부터 작성하라.
 중복을 제거하라.
Test Code 의 역할?
 Test Code 는 보조 수단이다
 프로그램 개발에 필수적인 요소
 문서
 설계
 개발 실력
 디버깅 능력
 Test Code 는 조금 더 안심할 수 있도록 할 뿐
 프로그램 개발의 필수 요소라고 볼 수 없다
TDD 에서 Test Code 의 역할!
 Test Code 는 필수적이다!
 Working Software 의 정의
 “올바르게” 동작하는 프로그램
 “올바르다” 의 정의는 어떻게 내리는가?
 그때 그때 실행해 보면서?
 No, 확고하게 설계한 Test Code 를 실행해서!
 Test Code 작성만으로도 대형 프로젝트가
얼마든지 진행될 수 있다!
Test 는 프로그램이
당연히 통과해야 하는 것
Test Code 작성은 프로그램 개발의
보조수단일 뿐이다
TDD Rhythm in 5 Steps
 Add a new TEST
 Run all TESTs, and check there is a FAILURE
 Slightly modify CODE
 Run all TESTs, and check everything SUCCESS
 Eliminate DUPLICATION by REFACTORING
TDD 에 따른 프로젝트의 삶
 고객과 함께 TO-DO List 를 작성한다
 리스트에서 할 일 하나을 고른다
 TDD Rhythm 을 타면서 그 일을 처리한다
 중간에 새로운 일이 생겨도 지금 처리중인 일에만
집중하고, 새로 생긴 일은 까먹지 않도록
단지 TO-DO List 에 올리기만 한다
 TO-DO List 가 빌 때까지 반복한다
 프로그램 개발이 끝난다
TO-DO LIST
할일 1
할일 2
할일 n …
TEST still SUCCESS
Refactored Code for 할일 i
TEST SUCCESS
할일 i
TEST Code for 할일 i
TEST Failure
Code for 할일 i
TDD Terminology
 Red bar period
 Period when one or more tests fail
 테스트 중 하나라도 통과하지 못한다
 Green bar period
 Period when entire tests success
 모든 테스트를 통과한다
 개발 진행 = 새로운 빨간막대  파란막대
Let’s see how it really works.
제한점
 어디까지나 TDD 를 “보여주기” 위한 것
 TDD 를 진행하면서 일어날 수 있는 다양한 상황
들을 위한 작위적인 예제임을 잊지 말자
 JUnit Framework 를 사용하여 Java 로 코딩
 Java 를 모르는 사람은 따라해보긴 힘들겠다
 Java 를 알면 알아서 프로젝트 생성 / 파일 추가
잘 하고 따라해보길 바람
 귀찮아서 Eclipse 스크린샷은 안 뜨겠음
Kent Beck’s Example
 Project Bank Account
 은행의 사용자 계좌 관리 프로그램을 개발하자
 고객은 다음과 같은 사항을 요구한다
 서로 다른 통화간에 덧셈이 가능해야 함
 계좌에 있는 돈을 특정 배로 늘릴 수 있어야 함
 Blah Blah Yada Yada …
 첫단계
 TO-DO List 부터 작성하자.
TO-DO List 의 작성법
 각각의 기능을 최대한 분해할 것
 새로운 기능은 최소한의 Test 로 검증가능해야!
 때때로 TO-DO List 에서 제거할 일도 있다
 다른 일을 처리하면서 자연스레 해결되는 경우.
TO-DO List 작성의 실제
 고객 요구 조건 (Recall)
 서로 다른 통화간에 덧셈이 가능해야 함
 계좌에 있는 돈을 특정 배로 늘릴 수 있어야 함
 두 계좌에 있는 돈이 동일함을 확인해야 함
 기능만 뽑아내어 다시 써보자!
 $5 + 10YEN = $6 (환율이 1:10 일 때)
 $5 x 2 = $10
 $5 = $5 (서로 다른 계좌)
할일 고르기
 원칙
 다른 일보다 먼저 진행될 수 있는 것부터
 다른 일보다 먼저 진행되어야 하는 것부터
 좀더 쉽고 단순한 일부터
 TODO List Recall
 $5 + 10YEN = $6 (환율이 1:10 일 때)
 $5 x 2 = $10  한 계좌에 대한 것. 너로 정했다
 $5 = $5 (서로 다른 계좌)
Test Code 설계
 Test Code 를 작성하며 결정되는 것들
 함수 / 클래스의 이름과 파라메터
 각 함수 / 클래스의 예상되는 동작
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}
Test 실행
 결과 확인
 테스트 실패 : 컴파일조차 되지 않는다.
 Dollar 라는 클래스가 정의되지 않음
 실패에 대한 대처법
 원인을 파악하고 가장 간단한 것부터 해결한다
 지금은 Dollar 클래스를 정의하면 충분하겠다
 times 메소드와 amount 필드가 있어야겠다
 어떻게든 이 테스트 코드를 통과시킬 수 없다면
 다른 할일부터 해결하는 것이 나을지도 모른다
Dollar 클래스 설계
Public class Dollar {
int amount;
public Dollar(int amount) {
}
void times(int multiplier) {
}
}
Test 실행
 결과 확인
 assertEquals(10, five.amount);
 결과가 0 이 나오고 있다
 assertEquals(A, B) 메소드
 A 의 값과 B 의 값이 일치할 때 통과하고,
일치하지 않을 때 실패하는 메소드이다
 five 객체의 amount 변수의 값이 10이 아니어서
통과하지 못한 것이다
상념에 대한 적절한 대처
 상념 1 : amount 멤버 변수는 개인 정보이니
private 로 지정되었어야 하지 않나?
 대처 1 : TODO 리스트에 다음을 추가한다
 Amount 를 private 로 만들기
 상념 2 : 실수 연산일텐데, 반올림 처리는?
 대처 2 : TODO 리스트에 다음을 추가한다
 Money 반올림?
 상념에 방해받지 말자. TODO List 가 기억!
Test 결과에 대한 적절한 대처
 한번의 코딩으로 초록 막대를 보지 못함을 슬
퍼할 이유는 전혀 없다
 결국은 초록 막대를 볼 것임을 우리는 안다
 가장 단순한 해결책을 찾는다
 five 객체의 amount 변수의 값이 10이 아니어서 통과
하지 못한 것이라고 했었다
 결론 : 객체의 amount 변수의 값이 10이어야!
가장 간단한 코드 수정
Public class Dollar {
int amount = 10;
public Dollar(int amount) {
}
void times(int multiplier) {
}
}
Test 실행
 결과
 모든 테스트가 성공한다
 의의
 오직 테스트를 통과시키기 위한 목적의 코딩.
 이런 코드는 사실 죄악이다.
그리고 그런 죄악을 저지르자. 왜?
 프로그램 개발 = 테스트 코드를 만들고
그 테스트를 통과시켜 초록 막대를 보는 거니까!
 그리고 죄악은 수습하면 그만이다.
죄악의 수습
 죄악을 저지르는 이유
 테스트 케이스에 대해 빨간 막대를 유지하면
 심리적인 불안감이 조성된다
 기분이 나쁘고 언짢다
 따라서 최대한 빨리 초록 막대를 보는 게 낫다
 죄악을 수습하는 방법
 처음부터 죄를 저지르지 않는다
 Refactoring 을 실시한다
Cf) Refactoring
 정의
 The process of changing a program's source code
 without modifying its external functional behavior
 in order to improve some of the
nonfunctional attributes of the software.
 간단히 말하면
 함수 / 파라메터 이름만 살짝 바꾸는 것
 함수의 내부 구조만 바꾸는 것
어떤 때 Refactoring 을 하는가
 확실한 성능의 개선이 있을 때
 예) 정렬을 담당하는 함수가 하나 있는데
 알고리즘을 Bubble Sort 로 했는데
 나중에 정렬하는 핵심 코드만 Quick Sort 로 바꾸기
 좀 더 가독성이 좋은 소스 코드로 바꿀 때
 예) 변수 이름이 a, b, c, d, 이런 식인데
 변수 이름을 gender, name, age, grade 하는 식으로
 좀더 그 기능과 연관되는 이름으로 바꾸기
TDD With Refactoring
 초록 막대 주기에서 Refactoring 을 하는 이유
 Refactoring 이후에도 초록 막대를 본다는 것은
 진짜로 External Behavior 는 바뀌지 않았음이
보장된다는 것을 의미
 Refactoring 하고 나서 빨간 막대가 나타난다는 것은
 뭔가 Refactoring 하다가 실수 했다는 뜻이니
실수를 바로잡을 기회가 생김
 TDD 방법론의 특성이 안전한 Refactoring 을
보장하는 셈이다
Typical TDD Refactoring
 죄악 수습용으로서의 Refactoring
 죄악들의 공통된 특징
 Test Code 에 쓰인 상수를
 프로그램 Code 에도 그대로 쓴다
 어떤 형태로든 중복이 발생한다
 코드의 중복은 나쁜 것
 코드의 중복을 제거한다!
 성능 개선을 위한 Refactoring
 성능 개선을 알아볼 수 있는 Test 도 추가하면 좋다
중복되는 코드 찾기, #1
 테스트 코드의 상수와 코드 내부의 상수
 밑줄 친 부분을 잘 관찰하자
…
Dollar five = new Dollar(5);
five.times(2);
…
…
int amount = 10;
…
5 * 2 = 10
 테스트 코드의 assertEqual 의 10은 사실
 New Dollar 의 5 와 five.times 의 2 의 곱이다!
…
Dollar five = new Dollar(5);
five.times(2);
…
…
int amount = 10;
…
중복이 더 잘 드러나도록
 amount 에 대응된 10 을 5 * 2 로 고쳤다
 이제 하나씩 하나씩 각개 격파해보자
…
Dollar five = new Dollar(5);
five.times(2);
…
…
int amount = 5 * 2;
…
중복되는 5가 등장하는 변수를
 상수와 같은 값을 지니는 변수를 찾자
 객체 생성자의 파라메터에 주목!
…
Dollar five = new Dollar(5);
…
…
int amount = 5 * 2;
public Dollar(int amount) {
…
변수를 이용하여 상수를 대체해
 생성자의 amount 파라메터로 넘어오니까
 굳이 Member variable 초기화에서 안해도 된다!
…
Dollar five = new Dollar(5);
…
…
int amount; // = 5 * 2;
public Dollar(int amount) {
this.amount = amount * 2;
}
…
Test 실행
 Test 실행
 매끄럽게 성공한다.
 죄악의 수습
 동일한 상수가 이곳 저곳에 있던 것을
 최소한 상수는 중복되지 않도록 수정해 나가면
 결국은 아름다운 코드가 나올 수 있게 된다!
 그래도 기왕이면 죄악을 저지르지 말자.
상수 2 를 없애보자
 상수와 같은 값을 갖는 변수 찾기가 관건!
…
five.times(2);
…
…
public Dollar(int amount) {
this.amount = amount * 2;
}
void times(int multiplier) {
}
…
파라메터 multiplier 발견
 용서없다. 죄악은 척살이다.
…
five.times(2);
…
…
public Dollar(int amount) {
this.amount = amount;// * 2;
}
void times(int multiplier) {
this.amount *= multiplier;
}
…
Test 실행
 결과
 결국 성공한다.
 더 리펙토링할 곳이 보이지 않는다
 죄악은 전부 수습되었다
 이 정도면 충분히 깔끔한 코드인 듯 싶다
 테스트도 전부 통과한다
 이번 TDD Rhythm 주기는 여기까지로 하자!
한 주기가 끝난 아름다운 소스
Public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
void times(int multiplier) {
this.amount *= multiplier;
}
}
TDD 는 매우 Agile 하다
 Comprehensive Documentation 보다
Working Software
 Test 를 통과하는 Software == Working Software
 무슨 수를 써서라도 Test 부터 통과시키자
 죄악을 저질러도 상관 없다
 용서받기만 하면 장땡이다
 함수의 구조에 대한 복잡한 설계 과정 없이
Test Code 에 맞추는 것만으로도 Working
Software 를 얻을 수 있다
TDD 는 매우 Agile 하다
 Follow Plan 보다 Respond to Change
 각각의 할 일들은 한번에 하나씩만 처리한다
 어느 것을 먼저 처리해도 문제될 것은 없다
 Test 만 작성할 수 있으면 된다
 고객 요구 조건의 변화는 결국 새로운 Test 의 추
가에 지나지 않는다
 그리고 그건 TDD Rhythm 한번 타면 해결된다
 Cf) TDD 에서는 기존의 Test Code 를 제거하는 것도 물
론 일어날 수 있는 일로 보고 있다.
 엄청나게 기민하다!
eXtreme Programming 과의 조화
 경제성
 “설계” 와 “테스트” 가 하나로 합쳐짐
 테스트 그 자체가 함수의 Interface 를 정의함!
 점진적 설계
 Test Code 는 한번에 하나씩 만드니 점진적 설계!
 자동화된 테스트
 TDD 는 언제나 자동화된 테스트를 만든다!
But TDD is too eXtreme!
 모든 프로그래밍을 TDD 로 할 필요가 있나?
 반드시 Test 를 먼저 짜야만 프로젝트가 진행될
수 있는 것만은 아니다.
 TDD 주기는 지나치게 엄격하다
 테스트 실패를 굳이 매번 확인할 필요는 없다
 언제나 “죄악” 부터 저지르고 리펙토링해?
 하나의 Test Code 만 통과시킨다고 해서 제대로
작동하는 프로그램을 못 만들 때도 있다
Just one more TDD Rhythm.
Recalling TO-DO List
 아까 한 개의 할일이 처리되었지만
 두 개의 할 일이 늘어났다
 TO-DO List
 $5 + 10YEN = $6 (환율이 1:10 일 때)
 $5 = $5 (서로 다른 계좌)  어쨌든 이걸 골랐다
 amount 를 private로
 이건 기존 Test Code 수정으로 충분할듯
 반올림?
Test Code 추가
 두 개의 Dollar 객체를 만들어서
 그 둘에게 equals 메소드를 적용하면
 true 값이 나오는지를 확인하자.
public void testEquality() {
assertTrue(
new Dollar(5).equals(
new Dollar(5));
}
Test 실행
 Test 실행
 당연히 실패한다.
 왜 굳이 실패해야 할까?
 어차피 새로운 Test 를 추가하면 실패하는 건 너
무 당연한 일이 아닐까?
 No! 이미 “잘못” 구현되어 있어야 하는 메소드가
있는데 그 메소드가 “잘못” 구현되어있다는 예측
을 벗어난다는 것을 검증하게 되는 경우도 있음
 그리고 Test 실패를 관찰하는 건 0.1초면 충분!
 그닥 시간낭비도 아니니까 걍 해주자. ㅋㅋ
마음놓고 죄악 저지르기
 최대한 단순하게 equals 메소드를 작성하자
Public class Dollar {
…
public boolean equals(Object object) {
return true;
}
}
Test 실행과 그 후 …
 결과
 당연히 … 잘 통과한다.
 Refactoring 할 거리를 찾아야 한다
 우리에게는 죄악이 만든 true 라는 상수가 있다
 분명히 이 상수는 다른 상수와 중복이다
 아까 10 = 5 * 2 였던 것처럼,
 이 true 도 뭔가 아주 당연한 연산에서 왔을 것
 근데 … 도저히 못찾겠다면 ??
난관에 봉착했다.
 원인 분석
 Refactoring 스킬이 부족하다면
 엄밀히 따지면 코드 중복이지만
 대충 봐서는 파악하기 힘든 경우에
 도저히 대처를 할 수가 없다
 대응 방법
 Refactoring 스킬을 조홀라 키운다
 Test Code 를 좀 더 잘 짠다
대응 방법 1을 따른다면
 일단 상수 true를 다른 상수로 표현해 보자.
 논리값 true 가 나올 수 있는 가장 대표적인 경우
라면 역시 “==“ 연산자 사용이다.
 Test Code 에는 동일한 상수 5 가 2회 나온다.
 이 상수들은 서로 다른 파라메터로 전달된다
 따라서 true 를 일단 5 == 5 로 고쳐보자
 다시 각각을 this.amount 와
 (Dollar)object.amount 로 고치는 게 가능하다
대응 방법 1의 한계점
 너무 뛰어난 추론능력이 필요하다
 추론은 곧 경험에서 우러나온다
 경험이 부족한 사람은 대응 방법 1로 할 수 없다
 경험이 쌓이기에는 시간이 걸린다
 TDD 초심자가 처음부터 저런 좋은 길을 걷기는
매우 힘들다
 결국 TDD 를 기피하게 될지도 …
아니, 대응 방법 2 로 가보자!
 TDD 원칙을 완전히 따르려면
 최초부터 테스트 코드를 작성할 적에
2가지 경우의 수를 고려해 본다
 TDD 스럽게 하기만 하면 된다면
 테스트 코드가 좀 부족했음을 인정하고
한꺼번에 통과시킬 테스트 코드를 … 추가하여
전체 테스트를 통과시키기 위해 노력해보자
 물론 이때는 지금까지의 죄악은 싸그리 초기화해
 쓸데 없는 죄악때문에 헷갈릴 수 있으니까
Test Code 추가부터 다시 간다
 처음부터 2 가지 경우를 고려해 보자!
public void testEquality() {
assertTrue(
new Dollar(5).equals(
new Dollar(5));
assertFalse(
new Dollar(5).equals(
new Dollar(6));
}
Test 실행
 결과는?
 당연히 실패한다.
 가장 단순한 해결책은?
 자세히 보니 true인가 false인가를 가르는
차이점이 하나 눈에 보인다
 메소드 eqlaus 의 파라메터로 들어오는 객체의
amount 값이 5인가 6인가에 달려 있다 !!
죄악을 저지르자
 괜찮아 이 모든 건 초록막대를 보고싶어서…
Public class Dollar {
…
}
public boolean equals(Object object) {
return (Dollar)object.amount == 5;
}
}
Test 실행
 결과
 잘 통과한다.
 같은 죄악이지만 급이 다르다
 Test Code 가 쓸 데 없이 둘이나 있는 듯 싶다
 나중에 Test Code 자체에 대한 Refactoring 도
하는 날이 올 것이다
 하지만 결과적으로 좀더 나은 코드를 가져왔다
 이런 Test Code 를 “삼각측량” 이라 한다
Refactoring 을 위해!
 상수의 중복이 훨씬 직관적으로 보인다
…
new Dollar(5).equals(
…
new Dollar(5).equals(
…
…
public Dollar(int amount) {
this.amount = amount;
…
public boolean equals(Object object) {
return (Dollar)object.amount == 5;
…
중복이 되는 5를 제거한다
 결국 이건 this.amount 다!
…
new Dollar(5).equals(
…
new Dollar(5).equals(
…
…
public Dollar(int amount) {
this.amount = amount;
…
public boolean equals(Object object) {
return (Dollar)object.amount ==
this.amount; // 5;
테스트 실행
 결과
 아주 잘 통과한다.
 또 하나의 주기가 끝나다!
 이런 식으로 계속 한 기능씩 한 기능씩 점진적
진행을 해 나가도록 하자.
 아무리 거창해 보이는 프로젝트라도 TDD 주기
단위로 쪼개버리면 별거 아니다.
Google loves TDD
 Google Testing Blog
 http://googletesting.blogspot.com/
 Testing on the Toilet
 Introducing numerous Testing Techniques
 http://googletesting.blogspot.com/2007/01/introd
ucing-testing-on-toilet.html
SUMMARY
Test-Driven Development (TDD) 이란,
코드 작성에 앞서 반드시 작성할 코드를 검증할
테스트 코드부터 작성한 뒤에 코드를 작성하는
것을 원칙으로 하는 프로그램 개발 방법론이다.
테스트 코드 추가, 실패, 간단한 구현, 테스트 통
과, 리펙토링의 순서로 개발을 진행한다.
TDD 철학대로만 강박관념을 갖고 코딩을 하는
것이 좋은 것만은 아니지만, 적어도 Testing 의
강력한 힘에 따라 XP 와 잘 섞어쓸 수 있다.
References
 Kent Beck, 테스트 주도 개발
 김창준, 강규영 역
 http://www.javajigi.net/pages/viewpage.actio
n?pageId=278