-
그냥 Spring에 저장함
Test - Driven Development : By Example
TDD를 쫒아가려 하지 말고 TDD가 나를 쫒아오게 해라
TDD 수련법
처음 훈련 방법
- 간단하고 쉬운 문제들을 TDD로 시도합니다. 가능하면 전에 접하고, 프로그래밍해본 문제가 좋습니다.
- 초록 막대 주기는 가능하면 짧도록 합니다. ex) a=b+3Xc를 입력하고자 할 때, a= 까지 입력한 시점은 빨간 막대 시점입니다. 그 시점에서 테스트를 실행하면 분명히 실패할 것이기 때문입니다. 가능한 초록 막대가 나오도록 해야 합니다.
- 이때 초록 막대 주기의 최대 시간을 정해놓고 진행하다가 시간을 초과하면 직전 초록 막대 상태로 돌린다음 새로 시작하는 것이 좋습니다.
- 진짜로 만들기 전까지만 가짜로 구현하기를 적극적으로 사용하려고 노력합니다. 가짜로 구현하기는 초록 막대 주기가 짧아지는 가장 간단하고 빠른 방법 입니다.
- 같은 문제를 여러번 풀어봅니다. 동일한 프로그램이 나오나요? 무엇이 비슷하고 무엇이 틀린가요? 뭘 배웠나요?
- 초기에는 리팩토링 툴을 사용하지 않는 것이 좋습니다. 초보에겐 순서와 과정이 중요합니다.
프로그래밍 순서
- 빨강 - 실패하는 작은 테스트를 작성한다. 처음에는 컴파일 조차 되지 않을 수 있음
- 초록 - 빨리 테스트가 통과하도록 만든다. 이를 위해 어떤 죄악을 저질러도 좋다.
- 리팩토링 - 일단 테스트를 통과하게만 하는 와중에 생겨난 모든 중복을 제거한다.
화폐 예제
TDD 리듬
- 재빠르게 테스트를 하나 추가한다.
- 테스트를 모두 실행 후 새로 추가한 것이 실패하는지 확인한다.
- 코드를 변경한다.
- 모든 테스트를 실행하고 전부 성공하는지 확인한다
- 리팩토링을 통해 중복을 제거한다.
다중 Money를 지원하는 화폐예제
위 첫 번째 테스트를 진행하면서 한 작업
- 우리가 알고 있는 작업해야 할 테스트 목록을 만들었다.
- 오퍼레이션이 외부에서 어떻게 보이길 원하는지 말해주는 이야기를 코드로 표현했다.
- JUnit에 대한 상세한 사항들은 잠시 무시
- 스텁 구현을 통해 테스트를 컴파일했다.
- 끔찍한 죄악을 범하여 테스틀 통과했다.
- 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화했다.
- 새로운 할일들을 한번에 처리하는 대신 할일 목록에 추가하고 넘어갔다.
타락한 객체
일반적인 TDD 주기는 다음과 같다
- 테스트를 작성한다. 마음속에 있는 오퍼레이션이 코드에 어떤 식으로 나타나길 원하는지 이야기를 써내려간다. 올바른 답으로 향하는 모든 요소를 포함시킨다.
- 실행 가능하게 만든다. 깔끔하고 단순한 해법이 명백히 보인다면 그것을 입력하라. 만약 깔끔하고 단순한 해법이 있지만 구현하는 데 몇 분 정도 걸릴 것 같으면 일단 적어 놓은 뒤에 원래 문제로 돌아오자. 빨리 초록 막대를 보는 것은 잠시동안 모든 죄를 사죄해준다.
- 이전 죄악을 수습하여 소프트웨어 정의의 길로 돌아와 중복을 제거하고 초록막대로 돌린다.
가장 빠른 초록색 막대를 보기위한 전략 3가지
- 가짜로 구현하기
- 상수를 반환하게 만들고 진짜 코드를 얻을 때까지 단계적으로 상수를 변수로 변경한다.
- 명백한 구현 사용하기
- 실제 구현을 입력한다.
- 삼각층량
우리가 한 방법
- 설계상 결함으로 실패하는 테스트 코드를 제작
- 스텁 구현으로 빠르게 컴파일을 통과하는 테스트 코드를 제작
- 올바르다고 생각하는 코드를 입력하여 테스트를 통과했다.
모두를 위한 평등
객체를 값처럼 사용하는 걸 값 객체 패턴이라고 한다. 해당 패턴에 제약사항으로는 객체의 인스턴스 변수가 생성자를 통해 설정된 후에는 결코 변하지 않아야 한다는 조건이 있다.
장점으로는 별칭 문제에 대해 걱정을 할 필요 없는 점이 존재하다. 값 객체가 암시하는걸로는 모든 연산 후 새로운 객체를 반환해야 한다는 것이 존재한다.
삼각측량은 예제가 2개 이상 있어야만 충족할 수 있는 방법이며, 리팩토링할 여지가 보이지 않는다면 사용하는 방법이다.정리
- 디자인 패턴이 하나의 오퍼레이션을 암시함
- 오퍼레이션을 테스트 및 구현
- 리팩토링 대신 테스트를 더 진행 후 삼각측량을 이용해 리팩토링
프라이버시
만약 동치성 테스트가 정확하게 동작하는 여부에 대한 검증이 실패한 경우 그 전에 제작한 곱하기 테스트 역시 정확하게 동작하는지 검증 테스트가 실패하게 된다. 이것이 TDD를 하면서 가장 많이 관리하는 위험 요소이다.
모든 것은 코드, 테스트 총 두 번 말함으로써 자신감을 가지고 전진할 수 있을 만큼만 결함의 정도를 나주기를 희망한다.정리
- 테스트를 향상시키기 위해 개발된 기능 사용
- 두 테스트가 실패 시 큰일난다는걸 판단
- 위험요소가 있어도 테스트
- 코드와 테스트의 결함도를 낮추기 위하여 테스트 하는 객체의 새 기능 사용
솔직히 말하자면
Ctrl + C / Ctrl + V
이번 챕터를 위해 진행한 코드 작업에 쓰인 기능이다.- 테스트 작성
- 컴파일되게 하기
- 실패하는지 확인
- 실행되게 재 제작
- 중복 제거
각 단계에는 목적이 있지만 처음 4 단계는 빨리 진행 되어야 한다. 그 동안 만큼은 속도가 설계보다 높은 패이기에 그렇기에 빠른 시간을 위해서는 어떤 죄악을 저질러야 한다.
정리
- 큰 테스트를 공략할 수 없기에 진전을 위해 작은 테스트 제작
- 중복 제작 후 고쳐서 테스트 작성
- 설성가상으로 모델 코드까지 도매금으로 복사하고 수정해서 테스트 통과
- 중복이 사라지기 전까지 코드 종료 X
돌아온 모두를 위한 평등
이전 5장에서 코드를 복사하는 작업을 진행했더니 수많은 중복 코드가 생겼다. 이를 해결하기 위해서는 공통 상위파일을 제작하여 중복코드를 제거할 수 있다.
코드를 변경 시 테스트 수행정리
- 공통된 코드를 Dollar 에서 상위 Money로 단계적으로 옮겼다.
- Franc 도 마찬가지로 Money 클래스의 하위 클래스로 만들었다.
- 불필요한 중복 코드인 equals() 를 일치시켜 제거 후 Money 클래스로 옮겼다.
사과와 오렌지
you can't compare apples and oranges
동치성 코드를 볼 경우 Dollar 와 Franc 가 서로 동일하다고 말하고 있다. 따라서 일단은 지저분하더라도getClass()
를 이용해 테스트를 통과하도록 만들었다.정리
- 결함을 끄집어내 테스트에 담아냈다.
- 완벽하지 않지만 그럭저럭 봐줄만한 방법으로 테스트를 통과하게 만들었다. (getClass())
- 더 많은 동기가 있기 전에는 더 많은 설계를 도입하지 않기로 했다.
객체 만들기
특정 클래스가 많은일을 하지 않아 삭제하고 싶더라도 한번에 큰 단계를 밟는 것은 TDD를 효과적으로 보여주기에 적합하지 않는다.
정리
- 동일한 메서드의 두 변이형 메서드 서명부를 통일시킴으로써 중복 제커를 향해 한 단계 더 전진했다.
- 최소한 메서드 선언부만이라도 공통 상위 클래스로 옮겼다.
- 팩토리 메서드를 도입하여 테스트 코드에서 콘크리트 하위 클래스의 존재 사실을 분리해냈다.
- 하위 클래스가 사라지면 몇몇 테스트는 불필요한 여분의 것이 된다는 것을 인식했다. (일단 삭제하지 않고 보류함)
우리가 사는 시간
시간 곱하기 시간은 time time ;;
TDD란 조종해 나가는 과정이다. 사람마다 걸음걸이가 다르기에 정답인 보폭은 존재하지 않는다.정리
- 큰 설계 아이디어를 다루기 전에 주목했었던 작은 작업을 수행
- 다른 부분들을 팩토리 메서드에 옮김으로써 Dollar, Franc를 일치 시켰다.
times()
가 팩토리 메서드를 사용하도록 만들기 위하여 리팩토링을 잠시 중단했다.- 비슷한 리팩토링을 한번에 큰 단계로 처리했다. (Franc에 했던 작업을 Dollar 에게도 적용)
- 동일한 생성자들을 상위 클래스로 올렸다.
흥미로운 시간
코드에 대한 긴가민가한 부분이 존재하다면 수정 후 테스트 코드를 돌려보자. 때로는 개발자가 한참 생각을 하는것 보다 테스트 코드에게 물어보는 것이 수백, 수천배 빠를 수 있다.
정리
- 두
times()
를 일치시키기 위하여 그 메서드들이 호출하는 다른 메서드 들을 인라인시킨 후 상수를 변수로 바꿔주었다. - 단지 디버깅을 위해 테스트 없이
toString()
을 추가했다. - Franc 대신 Money를 반환하도록 변경 후 잘 돌아갈지 생각보다는 테스트 코드를 돌렸다
- 실험해본걸 뒤로 물리고 또 다른 테스트 코드를 작성했다. 테스트를 작동했더니 실험도 제대로 작동했다.
모든 악의 근원
정리
- 하위 클래스의 속을 들어내는 걸 완료하고, 하위 클래스를 삭제했다.
- 기존의 소스 구조에서는 필요했지만 새로운 구조에서는 필요 없게 된 테스트를 제거했다.
드디어 더하기
객체가 우리를 구해줄 것이다. 가지고 있는 객체가 우리가 원하는 방식으로 동작하지 않을 경우엔 그 객체와 외부 프로토콜이 같으면서 내부 구현은 다른 새로운 객체(사칭 사기꾼)를 만들 수 있다.
정리
- 큰 테스트를 작은 테스트로 줄여서 발전을 나타낼 수 있다.
- 우리에게 필요한 계산에 대한 가능한 메타포들을 신중히 생각해봤다.
- 새 메타포에 기반하여 기존의 테스트를 재작성했다.
- 테스트를 컴파일 후 테스트를 실행했다.
- 진짜 구현을 제작하기 위해 리팩토링을 약간의 전율과 함께 기대했다.
진짜로 만들기
이번에는 이전과 다르게 거꾸로 작업해야 할지 분명하지 않다. 그래서 조금 불확실한 감이 있긴 하지만 순방향으로 작업을 진행한다.
정리
- 모든 중복이 제거되기 전까지는 테스트를 통과한 것으로 치지 않았다.
- 구현하기 위하여 역방향이 아닌 순방향으로 작업했다.
- 앞으로 필요할 것으로 예상되는 객체의 생성을 강요하기 윟나 테스트를 작성했다.
- 빠른 속도로 구현하기 시작했다.(Sum 생성자)
- 일단 한 곳에 캐스팅을 이용해서 코드를 구현했다가, 테스트가 돌아가자 그 코드를 적당한 자리로 옮겼다.
- 명시적인 클래스 검사를 제거하기 위해 다형성을 사용했다.
바꾸기
지금은 리팩토링하는 중 코드를 작성하는 것이기에 테스트를 작성하지 않을 것이다. 이 리팩토링을 마치고 테스트를 모두 통과한다면 그제서야 우리는 그 코드가 실제로 사용되었다고 할 수 있다.
0은 최악의 해시코드지만 구현하기 쉽고 빨리 달릴 수 있도록 도와준다. 하지만 이대로 해시코드를 방치한다면 해시 테이블에서의 검색이 마치 선형 검색과 비슷하게 수행될 것이다.
나중에 코드를 읽어볼 다른 사람들을 위해 (알려주기) 테스트로 만들자.정리
- 필요할 거라고 생각한 인자를 빠르게 추가했다.
- 코드와 테스트 사이에 있는 데이터 중복을 끄집어냈다.
- 자바의 오퍼레이션에 대한 가정을 검사해보기 위한 테스트를 작성했다.
- 별도의 테스트 없이 전용(private) 도우미 클래스를 만들었다.
- 리팩토링하다가 실수를 했고, 그 문제를 분리하기 위해 또 하나의 테스트를 작성하면서 계속 전진해 가기로 선택했다.
서로 다른 통화 더하기
앞으로의 테스트는 두 갈래 길로 나뉘어진다.
- 좁은 범위의 한정적인 테스트를 빠르게 작성한후에 일반화하는 방법
- 우리의 모든 실수를 컴파일러가 잡아줄 거라 믿고 진행하는 방법
정리
- 원하는 테스트를 작성하고, 한 단계에 달성할 수 있도록 뒤로 물렀다.
- 좀더 추상적인 선언을 통해 가지에서 뿌리로(애초의 테스트 케이스) 일반화 했다.
- 변경 후(Expression fiveBucks), 그 영향을 받은 다른 부분들을 변경하기 위해 컴파일러의 지시를 따랐다. (Expression에 plus 추가하기 등등)
드디어 추상화
테스트가 코드보다 더 길다. 그리고 코드는 Money의 코드와 거의 동일하다.
정리
- 미래에 코드를 읽는 사람을 위하여 코드 제작
- TDD와 현재 개발 스타일을 비교해 볼 수 있는 테스트 작성
- 또 한 선언부에 대한 수정이 시스템 나머지 부분으로 퍼져 나갔으며, 문제를 고치기 위한 컴파일 오류를 따랏다.
Money회고
1. 다음에 할 일은 무엇은인가
- 끝났다는 말을 믿지 않는다. TDD를 완벽을 위한 노력의 일환으로 사용할 수 있겠지만 그건 TDD의 가장 효과적인 용법이 아니다.
- 다음에 할건 뭐가 있는지?, 어떤 테스트가 추가로 필요한지?, 실패하는 이유는 무엇인지? 그 이유를 찾아야 한다. 이때는 이를 이미 알려진 제한사항 또는 앞으로 해야 할 작업등의 그 의미로 그 사실을 기록해둘 수도 있다.
- 할일이 진짜 없을경우 그때까지 설계한 것을 검토하기 적절한 시기이다.
2. 메타포
- 메타포라는 건 단지 이름들을 얻어 내는 데 필요한 것이 아니다.
3. JUnit 사용도
- JUnit 사용 히스토리를 볼 경우 조급한 리팩토링을 통한 테스트 실행은 의문의 성공 또는 실패가 나와 놀라게 했다.
4. 코드 메트릭스
- 코드와 테스트 사이에 대략 비슷한 양의 함수와 줄이 있는 것을 알 수 있다.
- 테스트 코드의 줄 수는 공통된 픽스처를 뽑아내 줄일 수 있다.
- 회기성 복잡도는 기존의 흐름 복잡도와 같다. 테스트 코드에 분기나 반복문이 전혀 없기 때문에 테스트 복잡도는 1이다. 명시적인 흐름 제어 대신 다형성을 주로 사용했기에 실제 코드의 복잡도 역시 낮다
5. TDD 프로세스 주기
- 작은 테스트를 추가
- 모든 테스트 실행 후 실패 테스트 확인
- 코드에 변화를 일으킴
- 모든 테스트 실행 후 성공 테스트 확인
- 중복을 제거하기 위한 리팩토링
6. 테스트의 질
- TDD의 부산물로 자연히 생기는 테스트들은 시스템의 수명이 다할 때까지 함께 유지돼야 할 만큼 확실히 유용하다. 하지만 이 테스트가 아래의 테스트를 대체할 수 있을 거라고 예상해서는 안된다.
- 성능 테스트
- 스트레스 테스트
- 사용성 테스트
- 테스트 지표
- 명령문 커버리지
- 테스트의 시작점이자 TDD는 100% 명령문 커버리지를 종교적으로 따른다
- 결함 삽입
- 테스트의 질을 평가하는 또 다른 방법
최종검토
TDD를 가르칠때 놀라는 세가지
- 테스트를 확실히 돌아가는 세가지 접근법
- 삼각측량
- 명백하게 구현사기
- 가짜로 구현하기
- 설계를 주도적으로 하기위한 방법으로는 테스트코드와 실제 코드의 중복을 제거하기
- 테스트 사이의 간격은 유동적으로 조절하자
'Spring' 카테고리의 다른 글
TDD[3] (0) 2021.10.08 TDD[2] (0) 2021.10.08 Spring Security[2] (0) 2021.07.19 Spring Security[1] (0) 2021.07.19 토비의 스프링[8] 스프링이란 무엇일까? (0) 2021.07.19