--- tags: test, tdd --- TDD 세미나 2부 - TDD === # 2부. TDD (Test-driven development) 기존의 개발방식은 `코드작성->테스트작성` 순서였다. 하지만 TDD는 반대로 `테스트작성->코드작성` 순으로 진행한다. 주기를 설명하자면 다음과 같다. - 테스트 작성 (컴파일에러 발생) - 컴파일 될만한 간단한 개발 - 테스트 실패 - 리팩터링 - 테스트 성공 ## 1. TDD 특징 원칙적으로 테스트없이 production 코드는 작성하지 않음. 그래서 하나의 메소드에 3~4개 씩의 검증을 하게 되고, 커버리지가 저절로 높아짐. 또, 테스트 친화적으로 코드가 작성 되게 되는데, 이말은 커뮤니케이션이 잘 되는 메소드라는 뜻. ### TDD 장점 1. 과도한 설계를 하지 않음 > 일단 assertThat 부터 쓰고봐도 됨 2. 양질의 테스트가 작성됨 3. 테스트 하기 쉬운 코드가 생산 됨 > 개발->테스트 방식보다 테스트가 주도하기 때문 ### TDD 단점 1. 많은 공수가 필요함. 중간중간 돌려보는 테스트와 리팩토링이 꽤 많음 2. 사전에 알아야할 지식들도 많고, 숙달하는데 시간이 필요함. 3. test suite 가 부족하면 손가락사이로 빠져나가는 버그 발생 ### TDD 는 필수는 아니다 개발관점에서의 장점은 테스트 결과물만 보면 *"높은 개발공수를 들여 잘짜여진 테스트코드를 얻는다"* 정도밖에 없다. 하지만 유지보수 관점에서 읽기 쉬운 테스트코드와 tdd를 진행하면서 저절로 생기는 객체지향적 객체들을 얻는 장점이 있다 ## 2. TDD 한 사이클 맛보기 TDD로 sum() 을 클래스화 해서 만들어보자. (1부에서 작성한 코드는 모두 지우자) 아무 구현도 없이 아래 테스트를 작성한다. ```java @Test public void sum_테스트(){ int sum = MyCalculator.sum(1,0); assertThat(sum).isEqualTo(1); } ``` MyCalculator 클래스가 없어 컴파일 에러가 나기 때문에 Test를 돌릴수 없다. 신택스에러만 빠르게 해결하자. ```java // MyCalculator 생성. public class MyCalculator { } ``` > /src/main/java 에 생성해야 하지만 편의를 위해 /src/test/java 에 생성하겠다. ```java // sum 메소드 작성 public static int sum(int i, int i1) { return 0; // stub } ``` 중요한것은 신택스 에러만 빨리 해결하는것 이다. 로직 구현은 리팩토링 단계로 미룬다. return 0; return 999999; 따위로 마무리 하면 된다. > 이렇게 테스트를 통과하기 위한 최소한의 값을 작성하는것을 `stubing` 이라고 한다. 테스트를 돌려보면 결과는 ``` Expected :1 Actual :0 ``` > "너는 1을 기대 하겠지만 결과는 0이야" 당연히 실패한다. sum() 을 `return 0;` 으로 IDE가 만들어주는데로 대충 만들었으니까. 가장 빠르게 성공할수 있는 코드를 넣어 리팩터링 해보자. ```java public static int sum(int i, int i1) { return 1; } ``` > return 1; return false; 와 마찬가지로 stub 이라고 할수 있다. > stub 은 mock 과 함께 테스트에서 자주 사용되는 용어이다. 다시 테스트 해보자. 초록막대가 보이면서 성공한다. 초록막대를 봤으니 이제 리팩토링을 할수 있다. ### 테스트 강화하기 리팩터링 하기에 앞서, 위 테스트는 뭔가 부족한 감이 있다. > 불길한 느낌을 감지하는것도 숙련이 필요하다. Test Suite 를 견고하게 하자. ```java @Test public void sum_테스트(){ int sum = MyCalculator.sum(1,0); assertThat(sum).isEqualTo(1); int sum1 = MyCalculator.sum(0,1); assertThat(sum1).isEqualTo(1); int sum2 = MyCalculator.sum(1,1); assertThat(sum2).isEqualTo(2); } ``` > 적은 테스트로 많은 의미를 담을수록 좋다. >처음부터 위 3셋트를 동시에 만들수도 있다. 이제 테스트를 돌려보면. ``` Expected :2 Actual :1 ``` > "너는 2를 기대 하겠지만 결과는 1야" 3번째 에서 통과하지 못했다. 작성한 코드가 잘못된 코드인 것이다. 이제 stubing을 걷어내고 확실한 구현을 하자. ```java // sum 메소드 작성 public static int sum(int a, int b) { return a+b; } ``` > 다시 테스트 해보면 성공한다. ### TDD 맛보기 정리 - TDD 주기를 1사이클 수행했다. - 1사이클을 작은 보폭으로 진행했다. - 테스트를 돌려보는데 아무런 거리낌이 없었다. - production코드를 생산하기 전에 반드시 테스트를 먼저 작성 했다. - return false; return 0; 처럼 점진적으로 나아갔다. - MyCalculator 클래스, sum() 메소드, 견고한 테스트 3개를 동시에 얻었다. ## 3. TDD - 기능 추가 더하기와 비슷하게 빼기 기능을 수행하는 `sub()` 를 추가 해보자. ```java @Test public void sub_테스트(){ int sub = MyCalculator.sub(1,0); assertThat(sub).isEqualTo(1); } ``` > 이번에도 당연히 sub() 을 구현하기 전에 테스트부터 작성 해야 한다. 테스트만 있으니 sub() 가 없다는 컴파일 에러가 발생했다. IDE의 도움을 받아 빠르게 컴파일만 되게끔 만들자. 이번에도 stubing 을 할까? 이번에는 stub 을 쓰지 않겠다. ```java // MyCalculator.java public static int sub(int a, int b) { return a-b; } ``` 1분 이내의 쉬운 구현이면 그냥 그자리에서 해버려도 된다. (sum 을 테스트 통과하면서 자신감이 생겼기 때문이기도 하다) 하지만 만약 조금이라도 고민이 필요하다면 Todo list 에 할일로 적어놓고 stub 부터 적용하여 작은 단계로 진행하는것을 추천한다. > 보폭을 줄이고 늘리고는 본인의 선택이다. > 어려운 일을 할땐 작은 보폭으로 진행하는게 많은 도움이 될 것이다. > 큰 보폭만 훈련하면 작은보폭을 하고싶어도 할수 없게된다. 테스트 돌려보면 성공한다. 이번에도 아직 테스트 스윗이 부족해 보인다. 아래처럼 견고하게 해보자. ```java @Test public void sub_테스트(){ int sub = MyCalculator.sub(1,0); assertThat(sub).isEqualTo(1); int sub1 = MyCalculator.sub(1,2); assertThat(sub1).isEqualTo(-1); int sub2 = MyCalculator.sub(2,2); assertThat(sub2).isEqualTo(0); } ``` 구현이 잘 되어 있어서 테스트가 한방에 성공한다. ## 4. TDD - 기능 변경 요구사항이 변해서 어떤 결과던 0~100 을 벗어날수 없다는것이 추가 되었다. 코드부터 바꾸고 싶겠지만 테스트부터 작성하자. ```java @Test public void sum의_결과는_0과100사이(){ assertThat(MyCalculator.sum(-1,-2)).isEqualTo(0); // -3 -> 0 assertThat(MyCalculator.sum(50,51)).isEqualTo(100); // 101 -> 100 } ``` 테스트 돌려보자. ``` Expected :0 Actual :-3 ``` > 방어코드가 아직 없으므로 테스트는 실패한다. 범위밖 값을 범위내 값으로 바꿔주는 메소드가 하나 있으면 좋을것 같다. 메소드를 만들기 전에 테스트를 만들자. ```java @Test public void changeValueWithinMinMax_범위밖값을_범위내값으로(){ int within = MyCalculator.getWithinMinMax(-3); assertThat(within).isEqualTo(0); int within2 = MyCalculator.getWithinMinMax(101); assertThat(within2).isEqualTo(100); } ``` getWithinMinMax 메소드가 없다는 컴파일에러가 난다. 아래와 같이 IDE를 이용해 빠르게 만들자. ```java public static int changeValueWithinMinMax(int i) { return 0; // stub } ``` 테스트 돌려보자. 당연히 실패한다. 리팩터링 하러 가자. ```java // MyCalculator.java public static int changeValueWithinMinMax(int i) { int MAXIMUM = 100; int MINIMUM = 0; if(i > MAXIMUM){ return MAXIMUM; } else if (i < MINIMUM) { return MINIMUM; } else { return i; } } ``` > 테스트 돌리면? 성공. 이제 아까 실패했던 sum의_결과는_0과100사이 를 getWithinMinMax() 를 이용해서 쉽게 리팩터링 할수 있다. ```java public static int sum(int a, int b) { return changeValueWithinMinMax(a+b); } ``` 이번엔 모든 테스트를 돌려서 성공하는지 확인하자. - sub_테스트 - sum_테스트 - sum의_결과는_0과100사이 - changeValueWithinMinMax_범위밖값을_범위내값으로 > 모두 초록막대인지 확인하자. MAXIMUM, MINIMUM 는 상수인데 메소드 안에 있는게 좀 아쉽지만 일단 한사이클은 종료했다. TDD는 `정상 동작하는 아름다운 코드` 에서, 아름다운은 나중이다. Todo list 에 추가 하고 다음에 리팩터링하면 된다. - **MAXIMUM, MINIMUM 은 상수이므로 멤버변수쪽으로 옮기기** 코드가 맘에 들지 않으면 언제든 리팩토링을 하면 된다. 초록막대 (테스트 성공) 를 띄우는 테스트 스윗이 있으므로 자신있게 진행하자. changeValueWithinMinMax 에 대한 테스트가 남아있으므로, 멤버변수쪽으로 옮기는것은 별도의 테스트케이스를 추가할 필요는 없을것 같다. 실습해보자. ## 실습 ### 실습1. sum과 sub를 직접 만들어보자. (가급적 타이핑하자) > 이번에는 sub도 sum처럼 stubing 과정을 거쳐서 진행할것 ### 실습2. Todo list 를 해결 해보자. 시간이 남으면 Todo list 에 곱하기 나누기를 추가하고 해결해보자.