Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

TDD 발담그기 @ 공감세미나

3,783 views

Published on

공감세미나에서 발표할 TDD 발담그기 자료입니다. TDD를 시작하려는 분들이 TDD를 감을 잡는데 약간의 도움이 될 만한 내용을 담았습니다.

Published in: Technology
  • Hello! Get Your Professional Job-Winning Resume Here - Check our website! https://vk.cc/818RFv
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

TDD 발담그기 @ 공감세미나

  1. 1. TDD 발담그기@공감세미나 최범균(madvirus@madvirus.net), 2017-05-13
  2. 2. 발표자 •최범균,madvirus@madvirus.net •주로자바로먹고살며, 잡다한분야에관심 •코딩잘하고,글잘쓰고싶은개발자 2TDD 발담그기@공감세미나 JPA 입문 곧 출간
  3. 3. 내용 •TDD발담그기 • 기능정의와TDD • 테스트코드의전형적인구성 • TDD협업대상도출 • 테스트범위 • TDD효과 • TDD시작과주의사항 •대상 • TDD를시작하려는개발자 3TDD 발담그기@공감세미나
  4. 4. 기능 •사용자가뭔가하면 •결과로뭘받음 4TDD 발담그기@공감세미나 사용자 인증 기능 사용자가 인증 시도를 하면 인증 결과로 성공/실패 여부를 받는다
  5. 5. 기능 • 어떤상황일때 • 정상인경우 • 사용자가정상요 청을하면 • 결과로정상응답 을받음 • 예외인경우 • 사용자가예외요 청을하면 • 결과로예외처리 응답을받음 TDD 발담그기@공감세미나 5 사용자인증기능 상황: 아이디가"bk"이고암호가"pw"인계정이존재할때 뭔가하면(정상인경우): 사용자가"bk","pw"로인증을요청하면 결과로뭘받음(정상인경우): 인증에성공함 뭔가하면(예외인경우): 사용자가"bk","no"로인증을요청하면 결과로뭘받음(예외인경우): 인증에실패함
  6. 6. 순서 •기능명세명세에맞게구현진행 • 구현을진행하면서명세를조정 •구현먼저기능명세 6TDD 발담그기@공감세미나
  7. 7. 무엇을(what) 만들지가 어떻게(how) 만들지보다 우선 7TDD 발담그기@공감세미나
  8. 8. 테스트 주도 개발 Test-Driven Development •테스트를먼저만들고 테스트를통과하는구현을작성 • 테스트코드부터시작 • 테스트없이제품코드작성하지않음 •테스트를통과할만큼의코드만작성 • 불필요한코드작성최소화 • 과도한설계방지 8TDD 발담그기@공감세미나
  9. 9. 야구 게임 예 •야구게임규칙 • 숫자3개를맞추면이김 • 각숫자는겹치지않음 • 예측한숫자가존재할때 • 위치가같으면스트라이크 • 위치가다르면볼 9TDD 발담그기@공감세미나
  10. 10. 야구 게임 예 •정답이371일때 • 예측한숫자가582이면0볼0스트라이크 • 예측한숫자가371이면3스트라이크 • 예측한숫자가389이면1스트라이크 • 예측한숫자가317이면2볼1스트라이크 10TDD 발담그기@공감세미나
  11. 11. 야구 게임 예: 테스트 코드로 시작 @Test publicvoidnomatch(){ //정답이479인게임에서 Gamegame=newGame(479); //123을예측하면 Scores=game.guess(123); //0스트라이크,0볼 assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); } 11TDD 발담그기@공감세미나
  12. 12. 테스트를 통과할 만큼만 작성 @Test publicvoidnomatch(){ //정답이479인게임에서 Gamegame=newGame(479); //123을예측하면 Scores=game.guess(123); //0스트라이크,0볼 assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); } publicclassGame{ publicGame(intvalue){ } publicScoreguess(intguess){ returnScore.create(0,0); } } publicclassScore{ privatescnt,bcnt; ...생성자,create() publicintstrikes(){returnscnt;} publicintballs(){returnbcnt;} } 12TDD 발담그기@공감세미나
  13. 13. 일치하지 않는 경우 추가 @Test publicvoidnomatch(){ Gamegame=newGame(479); Scores=game.guess(123); assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); Scores2=game.guess(568); assertThat(s2.strikes()).isEqualTo(0); assertThat(s2.balls()).isEqualTo(0); } publicclassGame{ publicGame(intvalue){ } publicScoreguess(intguess){ returnScore.create(0,0); } } publicclassScore{ … } 13TDD 발담그기@공감세미나
  14. 14. 코드 정리 @Test publicvoidnomatch(){ Gamegame=newGame(479); assertNoMatch(game.guess(123)); assertNoMatch(game.guess(568)); } publicvoidassertNoMatch(Scores){ assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); } 14TDD 발담그기@공감세미나 @Test publicvoidnomatch(){ Gamegame=newGame(479); Scores=game.guess(123); assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); Scores2=game.guess(568); assertThat(s2.strikes()).isEqualTo(0); assertThat(s2.balls()).isEqualTo(0); }
  15. 15. 일치하지 않는 경우 추가 @Test publicvoidnomatch(){ Gamegame=newGame(479); assertNoMatch(game.guess(123)); assertNoMatch(game.guess(568)); assertNoMatch(game.guess(321)); } publicvoidassertNoMatch(Scores){ assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); } publicclassGame{ publicGame(intvalue){ } publicScoreguess(intguess){ returnScore.create(0,0); } } publicclassScore{ … } 15TDD 발담그기@공감세미나
  16. 16. 모두 일치하는 경우 @Test publicvoidallstrikes(){ Gamegame=newGame(479); Scores=game.guess(479)); assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); } publicclassGame{ publicGame(intvalue){ } publicScoreguess(intguess){ if(guess==479) returnScore.create(0,3); returnScore.create(0,0); } } 16TDD 발담그기@공감세미나
  17. 17. 모두 일치하는 경우 추가 @Test publicvoidallstrikes(){ Gamegame=newGame(479); Scores=game.guess(479)); assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); Gamegame2=newGame(124); Scores=game.guess(124)); assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); } 17TDD 발담그기@공감세미나 publicclassGame{ privateintvalue; publicGame(intvalue){ this.value=value; } publicScoreguess(intguess){ if(value==guess) returnScore.create(0,3); returnScore.create(0,0); } }
  18. 18. 코드 정리 @Test publicvoidallstrikes(){ Gamegame=newGame(479); assertAllStrikes(game.guess(479)); Gamegame2=newGame(124); assertAllStrikes(game2.guess(124)); } privateassertAllStrikes(Scores){ assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); } 18TDD 발담그기@공감세미나 @Test publicvoidallstrikes(){ Gamegame=newGame(479); Scores=game.guess(479)); assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); Gamegame2=newGame(124); Scores=game.guess(124)); assertThat(s.strikes()).isEqualTo(3); assertThat(s.balls()).isEqualTo(0); }
  19. 19. 일부 일치하는 경우 @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); } privateassertMatch(Scores,intb,ints){ assertThat(s.strikes()).isEqualTo(s); assertThat(s.balls()).isEqualTo(b); } publicclassGame{ … publicScoreguess(intguess){ if(value==guess) returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } returnScore.create(0,scnt); } } 19TDD 발담그기@공감세미나
  20. 20. 일부 일치하는 경우 추가 @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); assertMatch(game.guess(372),0,1); } … publicclassGame{ … publicScoreguess(intguess){ if(value==guess) returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } returnScore.create(0,scnt); } 20TDD 발담그기@공감세미나
  21. 21. 일부 일치하는 경우 추가 @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); assertMatch(game.guess(372),0,1); assertMatch(game.guess(486),0,1); } … publicclassGame{ … publicScoreguess(intguess){ if(value==guess) returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } if(value/100==guest/100){ scnt+=1; } returnScore.create(0,scnt); } 21TDD 발담그기@공감세미나
  22. 22. 경우 추가 @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); assertMatch(game.guess(372),0,1); assertMatch(game.guess(486),0,1); assertMatch(game.guess(478),0,2); assertMatch(game.guess(429),0,2); assertMatch(game.guess(379),0,2); assertMatch(game.guess(489),0,2); } … publicclassGame{ … publicScoreguess(intguess){ if(value==guess)returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } if(value/100==guest/100){ scnt+=1; } returnScore.create(0,scnt); } 22TDD 발담그기@공감세미나
  23. 23. 코드 정리 23TDD 발담그기@공감세미나 publicclassGame{ … publicScoreguess(intguess){ if(value==guess)returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } if(value/100==guest/100){ scnt+=1; } returnScore.create(0,scnt); } publicclassGame{ … publicScoreguess(intguess){ if(value==guess)returnScore.create(0,3); intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } if(value/100==guest/100){ scnt+=1; } returnScore.create(0,scnt); }
  24. 24. 코드 정리 publicclassGame{ … publicScoreguess(intguess){ intscnt=0; if(pos(value,1)==pos(guess,1))scnt+=1; if(pos(value,2)==pos(guest,2))scnt+=1; if(pos(value,3)==pos(guest,3))scnt+=1; returnScore.create(0,scnt); } privateintpos(intv,intp){ switch(p){ case1:returnv%10; case2:returnv/10%10; case3:returnv/100; } thrownewIllegalArgumentException(); } 24TDD 발담그기@공감세미나 publicclassGame{ … publicScoreguess(intguess){ intscnt=0; if(value%10==guess%10){ scnt+=1; } if(value/10%10==guest/10%10){ scnt+=1; } if(value/100==guest/100){ scnt+=1; } returnScore.create(0,scnt); }
  25. 25. 코드 정리 @Test publicvoidnomatch(){ Gamegame=newGame(479); assertNoMatch(game.guess(123)); … } @Test publicvoidallstrikes(){ Gamegame=newGame(479); assertAllStrikes(game.guess(479)); Gamegame2=newGame(124); assertAllStrikes(game2.guess(124)); } @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); … assertMatch(game.guess(478),0,2); … } publicclassGame{ … publicScoreguess(intguess){ intscnt=0; for(intvi=1;vi<=3;vi++){ if(pos(value,vi)==pos(guess,vi)) scnt++; } returnScore.create(0,scnt); } … 25TDD 발담그기@공감세미나
  26. 26. TDD 과정 테스트 작성 코드 작성 코드 정리 26TDD 발담그기@공감세미나
  27. 27. 테스트 코드 = 기능 명세 •테스트코드는무엇을만들지에초점 • 구현을먼저하지않음 •다양한경우에대한테스트를점진적으 로추가 • 다양한경우곧예제기반명세 •테스트코드를통과시키는 과정에서구 현이완성 27TDD 발담그기@공감세미나
  28. 28. @Test publicvoidnomatch(){ Gamegame=newGame(479); assertNoMatch(game.guess(123)); assertNoMatch(game.guess(568)); assertNoMatch(game.guess(321)); } @Test publicvoidallstrikes(){ Gamegame=newGame(479); assertAllStrikes(game.guess(479)); Gamegame2=newGame(124); assertAllStrikes(game2.guess(124)); } @Test publicvoidsomestrikes(){ Gamegame=newGame(479); assertMatch(game.guess(359),0,1); assertMatch(game.guess(372),0,1); assertMatch(game.guess(486),0,1); assertMatch(game.guess(478),0,2); assertMatch(game.guess(429),0,2); assertMatch(game.guess(379),0,2); assertMatch(game.guess(489),0,2); } 테스트에 경우(예)를추가하면서 점차구현을완성 28TDD 발담그기@공감세미나
  29. 29. 테스트 코드의 전형적인 구성 •세개로구성 • 상황/조건(Given) • 기능실행(When) • 결과(Then) • 검증,확인 @Test publicvoidnomatch(){ //Given:정답이479일때 Gamegame=newGame(479); //When:123을예측하면 Scores=game.guess(123); //Then:0스트라이크,0볼 assertThat(s.strikes()).isEqualTo(0); assertThat(s.balls()).isEqualTo(0); } 29TDD 발담그기@공감세미나
  30. 30. 테스트 코드 작성 예 // Given: 비정상 계좌 givenAbnormalAccount("111222333444"); // When: 비정상 계좌로 자동 이체를 신청 ApplyReq applyReq = ApplyReq.builder(). .accountNum("111222333444").userId("myuser") .socialNum("060101").build(); ApplyResult rst = autoDebitSvc.applyAutoDebit(applyReq); // Then: 자동 이체 신청에 실패해야 함 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); 30TDD 발담그기@공감세미나
  31. 31. // Given: 비정상 계좌 givenAbnormalAccount("111222333444"); // When: 비정상 계좌로 자동 이체를 신청 ApplyReq applyReq = ApplyReq.builder(). .accountNum("111222333444") .userId("myuser") .socialNum("060101").build(); ApplyResult rst = autoDebitSvc.applyAutoDebit(applyReq); // Then: 자동 이체 신청에 실패해야 함 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); 테스트 코드 작성  설계 고민 필요 테스트 대상의 타입 이름과 메서드 이름 고민 기능에 전달할 입력 데이터 고민 결과를 검증하려면 기능의 응답 방식 고민 계좌 비정상 여부를 어떻게 지정 31TDD 발담그기@공감세미나
  32. 32. 테스트 코드 작성  설계 고민 필요 은행에 통지한다는 걸 어떻게 확인할 수 있나 @Test publicvoid정상계좌면_신청성공_은행통지(){ //Given:정상계좌 givenNormalAccount("111222333444"); // When: 정상 계좌로 자동 이체를 신청 ApplyReq applyReq = ApplyReq.builder(). .accountNum("111222333444") .userId("myuser") .socialNum("060101").build(); ApplyResult rst = autoDebitSvc.applyAutoDebit(applyReq); //Then:은행에자동이체계좌통지해야함 bankShouldBeNotified("111222333444"); } 32TDD 발담그기@공감세미나
  33. 33. 테스트 코드와 협업 객체 도출 상황/조건에서 테스트 대상의 협업 객체 도출 가능성 검토 // Given: 비정상 계좌 givenAbnormalAccount("111222333444"); // When: 비정상 계좌로 자동 이체를 신청 ApplyReqapplyReq = ApplyReq.builder(). .accountNum("111222333444").userId("myuser") .socialNum("060101").build(); ApplyResultrst = autoDebitSvc.applyAutoDebit(applyReq); // Then: 자동 이체 신청에 실패해야 함 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); 33TDD 발담그기@공감세미나
  34. 34. // Given: 비정상 계좌 givenAbnormalAccount("111222333444"); // When: 비정상 계좌로 자동 이체를 신청 ApplyReqapplyReq = ApplyReq.builder(). .accountNum("111222333444").userId("myuser") .socialNum("060101").build(); ApplyResultrst = autoDebitSvc.applyAutoDebit(applyReq); // Then: 자동 이체 신청에 실패해야 함 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); 협업 객체를 위한 타입 도출 AutoDebitService AccountValidator 그 시점에 적당해 보이는 용어 사용 34TDD 발담그기@공감세미나
  35. 35. 검증/확인에서 협업 객체 도출 @Test publicvoid정상계좌면_신청성공_은행통지(){ //Given:정상계좌 givenNormalAccount("111222333444"); …생략 //Then:은행에자동이체계좌통지해야함 bankShouldBeNotified("111222333444"); } privatevoidbankShouldBeNotified(StringaccNum){ ???? } publicinterfaceBankNotifier{ voidnotify(StringaccNum); } 35TDD 발담그기@공감세미나
  36. 36. 테스트에서 협업 대상 도출 •협업객체도출설계 • 역할분리 • 테스트코드가설계유도 •필요한만큼의추상화 • 과하게추상화하는것을방지 • 추상화하지않는것도방지 36TDD 발담그기@공감세미나
  37. 37. 협업 객체는 일단 대역(double)으로 대치 당장은 테스트 통과가 우선 privateAutoDebitServiceautoDebitSvc=newAutoDebitService(); privateFakeAccountValidatorfakeValidator=newFakeAccountValidator(); @Before publicvoidsetup(){ autoDebitSvc.setAccountValidator(fakeValidator); } @Test publicvoid비정상계좌는_자동이체신청_실패(){ //Given:비정상계좌 givenAbnormalAccount("111222333444"); //When:비정상계좌로자동이체를신청 ApplyReqapplyReq=ApplyReq.builder(). .accountNum("111222333444").userId("myuser") .socialNum("060101").build(); ApplyResultrst=autoDebitSvc.applyAutoDebit(applyReq); //Then:자동이체신청에실패 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); } 여기서 테스트하고 싶은 것은? 자동이체신청 기능 자체임 계좌를 검증하는 기능은 아님! 테스트 대상이 아니면 협업 객체인지 고민 37TDD 발담그기@공감세미나
  38. 38. 진짜처럼 행동하는 가짜 대역 publicinterfaceAccountValidator{ AccountTypevalidate(StringaccNum); } publicclassFakeAccountValidator implementsAccountValidator{ privateMap<String,AccountType>map= newHashMap<>(); publicvoidadd(StringaccountNum,AccountTypetype){ map.put(accountNum,type); } @Override publicAccountTypevalidate(StringaccNum){ returnmap.getOrDefault(accNum,AccountType.NONE); } } @Test publicvoid비정상계좌는_자동이체신청_실패(){ //Given:비정상계좌 givenAbnormalAccount("111222333444"); … } voidgivenAbnormalAccount(StringaccNum){ fakeValidator.add(accNum, AccountType.ABNORMAL); } 38TDD 발담그기@공감세미나
  39. 39. 가짜 대역으로 일단 테스트 통과 @Test publicvoid비정상계좌는_자동이체신청_실패(){ //Given:비정상계좌 givenAbnormalAccount("111222333444"); //When:비정상계좌로자동이체를신청 ApplyReqapplyReq=ApplyReq.builder(). .accountNum("111222333444") …생략.build(); ApplyResultrst= autoDebitSvc.applyAutoDebit(applyReq); //Then:자동이체신청에실패 assertThat(rst.isSuccess()).isFalse(); assertThat(rst.getFailCause()) .isEqualTo(FailCause.ABNORMAL_ACCOUNT); } private void givenAbnormalAccount(String accNum) { fakeValidator.add(accNum, AccountType.ABNORMAL); } publicclassAutoDebitService{ privateAccountValidatoraccValidator; publicApplyResult applyAutoDebit(ApplyReqreq){ AccountTypetype= accValidator.validate(req.getAccountNum()); if(type==AccountType.ABNORMAL){ returnApplyResult.fail( FailCause.ABNORMAL_ACCOUNT); } returnnull; } …setter } 39TDD 발담그기@공감세미나
  40. 40. 모의(Mock) 대역으로 테스트 통과 privateAutoDebitServiceautoDebitSvc=newAutoDebitService(); privateBankNotifiermockBankNotifier=mock(BankNotifier.class); …@Before에서autoDebitSvc.setBankNotifier(mockBankNofitier); @Test publicvoid정상계좌면_신청성공_은행통지(){ //Given:정상계좌 givenNormalAccount("111222333444"); …생략 //Then:은행에자동이체계좌통지해야함 bankShouldBeNotified("111222333444"); } privatevoid bankShouldBeNotified(StringaccNum){ verify(mockBankNotifier).notify(accNum); } 40TDD 발담그기@공감세미나 publicclassAutoDebitService{ privateAccountValidatoraccValidator; privateBankNotifierbankNotifier; publicApplyResult applyAutoDebit(ApplyReqreq){ AccountTypetype= accValidator.validate(req.getAccountNum()); if(type==AccountType.ABNORMAL){ returnApplyResult.fail( FailCause.ABNORMAL_ACCOUNT); } bankNotifier.notify(req.getAccountNum()); returnApplyResult.success(); } …setter }
  41. 41. 대역의 이점 •협업객체의실제구현없이 테스트대상구현,검증가능 • 가짜,모의,메모리구현사용 •다양한상황을쉽게구성 •테스트속도가빨라짐 41TDD 발담그기@공감세미나
  42. 42. 테스트 범위 확대 •작은범위를테스트하는 단위테스트만 으로는부족 •각구성요소를통합한테스트필요 • DB,외부라이브러리,API연동등 •사용자입장에서기능이올바르게동작 하는지검증하는테스트필요 • 웹브라우저를포함한전체시스템 42TDD 발담그기@공감세미나
  43. 43. 테스트 범위 SVC Domain UI Infra DB 외부 브라우저 43TDD 발담그기@공감세미나
  44. 44. 테스트 코드의 효과 •자신감 • 회귀(regression) 테스트 •개발속도 • 대역으로격리개발가능 • 다양한경우를위한테스트로디버깅유리 •좋은설계가능성 • 테스트가능한구조를만드는시도 알맞은역할분리 44TDD 발담그기@공감세미나
  45. 45. TDD 시작 •쉬운경우부터작성 • 예,야구게임에서볼보다스트라이크판 단이쉬움 •예외적인경우부터작성 • 예,야구게임에서입력한숫자가3자리가 아닌경우익셉션처리하기 •어렵거나정상적인경우부터시작하면 전진을못하고중간에막히는상황발생 45TDD 발담그기@공감세미나
  46. 46. TDD 시작 •작은기능으로TDD감잡아보기 •테스트코드로협업객체도출하기 • 대역으로테스트상황(given) 구성시도 • 대역으로테스트검증/확인(then) 시도 •서비스나컨트롤러(UI)를 대상으로 통합테스트만들어보기 • 남이만든코드복붙해서빨리해보기 46TDD 발담그기@공감세미나
  47. 47. 주의 사항 • 방해꾼:"이런걸왜해?빨리개발이나해!" • 귓등으로듣고,계속시도할것 • 불안심리:"이거하고있을시간이…" • 결국개발자는테스트를해야함 • 실제로는테스트코드덕에개발시간을줄일 수있음 • 혼자서잘못하고TDD 탓하기: "TDD했더니되려개발을못하는것같아" • 잘못된자세로운동해서몸아픈것과같음 • 좋은책,좋은개발자가시키는대로하기 47TDD 발담그기@공감세미나
  48. 48. TDD 수련은 운동과 같음 •처음시작은어색하고힘듦 •올바른자세로꾸준히해야 몸에붙고코드가건강해짐 •5일하고360일쉬고그러면안됨 48TDD 발담그기@공감세미나
  49. 49. 시작을 위한 추천 도서 TDD 발담그기@공감세미나 49 테스트 주도 개발 Effective Unit Testing
  50. 50. 끝 최범균 | madvirus@madvirus.net | http://javacan.tistory.com 50TDD 발담그기@공감세미나

×