Clean Code 3판을 읽고 정리한 글입니다
Ⅸ. 단위 테스트
1997이전
- TDD 개념 없었음
- 단위 테스트
- 자기 프로그램이 돌아간다는 사실만 확인하는 일회성 코드
- 클래스 메서드를 공들여 구현한 후, 임시 코드를 급조해 테스트 수행
현재
- 테스트 코드 : 코드의 구현을 모두 확인하는 테스트 코드
- 애자일과 TDD → 단위테스트 자동화 이미 많아짐
- 급한 테스트 추가 + 제대로 된 테스트 케이스 작성을 놓침
TDD 법칙 3가지
3가지 법칙
- 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다
- 컵파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트 작성
- 현재 실패하는 테스트를 통과할 정도로만 실제 코드 작성
→ 3법칙 따르면 개발과 테스트가 대략 30초 주기
→ 테스트 코드가 실제 코드보다 불과 몇 초전
→ 매일 수십개, 매달 수백개, 매년 수천 개에 해당하는 테스트 케이스
→ 실제 코드를 사실상 전부 테스트 하는 테스트 케이스
→ 때로는 심각한 관리 무제 유발
깨끗한 코드 유지하기
테스트 코드는 지저분해도 빨리?
- 실제 코드 진화 → 테스트도 진화 필요
- 테스트 코드가 지저분하면 변경이 어려움 → 테스트 코드 추가/수정이 실제 코드 짜는 시간보다 오래걸림
- 테스트 코드는 실제 코드 못지 않게 중요하다
- 테스트 코드도 구현 코드처럼 깨끗하게 짜야 한다
테스트는 유연성, 유지보수성, 재사용성 제공
- 테스트 케이스가 있으면 변경에 대한 공포없이 안심하고 개선 가능
- 테스트는 변경되는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠
Clean Test Code
- 테스트 코드의 가장 중요한 점? 가독성
- 실제코드보다 테스트 코드에서 가독성은 더 중요
- 높은 가독성? → 명료성, 단순성, 붕푸한 표현력
1 | // FitNess에서 가져온 코드 |
문제점
- addPage와 assertSubString을 부르느라 중복되는 코드가 매우 많다
PathParser
- 문자열을 pagePath 인스턴스로 변환
- pagePath 웹 로봇(크롤러)가 사용하는 객체 → 테스트와 무관하며 테스트 의도만 흐린다
- Responder생성, respoonse 수집 코드 → 역시 잡음
- resource의 인수에서 요청 URL 을 만드는 어설픈 코드
- 읽는 사람을 고려하지 않음
1 | public void testGetPageHierarchyAsXml() throws Exception { |
BUILD-OPERATE-CHECK 패턴이 위와 같은 테스트 구조에 적합
- 각 테스트는 명확히 세 부분으로 나눠진다.
- 첫 부분은 테스트 자료를 만든다.
- 두 번째 부분은 테스트 자료를 조작
- 세 번째 부분은 조작한 결과가 올바른지 확인
테스트 코드는 본론에 돌입해 진짜 필요한 자료유형과 함수만 사용
- 잡다하고 세세한 코드를 거의 다 없앰
- 읽는 사람으로 하여금 테스트 코드가 수행하는 기능을 재빨리 이해
도메인 특화된 테스트 언어
- 위의 리팩토링된 코드는 DSL로 테스트 코드 구현 기법 보여줌
- 흔히 쓰는 시스템 조작 API 사용대신 API 위에 함수, 유틸리티 구현후 사용
- 이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 된다
→ 테스트 구현 당사자, 테스트를 읽어볼 독자를 도와주는 테스트 언어 - 테스트 API는 처음부터 설계된 API가 아님 → 리팩터링하며 진화된 API
이중 표준
- 테스트 API에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다
- 단순,간결,표현력 풍부는 동일
- 실제 코드만큼 효율적일 필요는 없음 → 테스트 환경에서 돌아가는 코드이므로
1 | //대충 읽어도 온도가 급격히 떨어지면 경보, 온풍기, 송풍기 확인하는 테스트 코드 |
1 |
|
설명
wayTooCold
함수로 기존tic
함수 숨김- 그릇된 코드 위반에 가깝지만 여기에 적절 → 테스트 코드가 읽기 쉬어짐
1 |
|
- 테스트 코드 이해가 너무 쉬어짐
1 | public String getState() { |
- 성능적으로 효율적이지 못한 코드
- 효율을 높이려면
StringBuffer
가 더 작합하지만 보기에 흉하다 - 위 코드는
StringBuffer
를 사용하지 않아 치루는 비용이 미미→ 테스트에선 자원 제한일 가능성이 낮으므로
이중 표준의 본질
- 실제 환경에서는 절대 안되지만 테스트 환경에서는 전혀 문제 없는 방식이 존재
- 대게 메모리,cpu효율 관련
- 클린 코드 여부와는 철저히 무관
테스트당 assert 하나
테스트 코드시 함수마다 assert 하나만 사용?
- 그래야 한다는 파 존재
- 확실한 장점이 있음 → 결론이 하나라 코드가 이해하기 쉽고 빠름
- 모두 적용되지 않음
- 위 9-2_리팩터링 코드는
- “출력이 XML”이다라는 assert와
- “특정 문자열을 포함”이라는 assert가 있으며
- 이 둘을 하나로 병합하는 방식이 불합리해보임
- 방법 : 테스트를 쪼개 각자가 assert 수행
1 | public void testGetPageHierarchyAsXml() throws Exception { |
- given-when-then 관례 사용
- 테스트 코드 일기가 쉬워짐
- 불행히도 위에서 보듯이 테스트를 분리하면 중복된 코드가 많아짐
- 해결? 템플릿 메서드 패턴 사용시 중복 제거 가능
- 템플릿 메서드 패턴 : 중복 제거 가능
- given/when을 부모 클래스에
- then을 자식 클래스에
- 다른 방법?
- 독자적 클래스를 만들고
@Before
에 given when을 넣고@Test
에 then부분을 넣어도 된다
- 독자적 클래스를 만들고
- 문제는? 모두가 배보다 배꼽이 크다
- 모든걸 감안하면 결국 9-2처럼 assert을 여러 개를 사용하는 것이 좋다
테스트당 개념 하나
→테스트 함수마다 한 개념만 테스트하라
1 | /** |
바람직하지 못한 코드
- 독자적인 개념 3개를 테스트 → 독자적 테스트 3개로 쪼개야
- 한 함수일경우 독자가 각 절이 거기에 존재하는 이유와 각 절이 테스트하는 개념을 모두 이해해야 한다.
분리한 테스트 코드
- (5월처럼) 31일로 끝나는 달의 마지막 날짜가 주어지는 경우
- (6월처럼) 30일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안 된다.
- 두 달을 더하면 그리고 두 번째 달이 31일로 끝나면 날짜는 31일이 되어야 한다.
- (6월처럼) 30일로 끝나는 달의 마지막 날짜가 주어지는 경우
- 31일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되면 안 된다.
분리하면? → 감춰진 일반적인 규칙이 보임
- 날짜에 어떤 달을 더 하면 날짜는 그달의 마지막 날짜보다 커지지 못함
→ 2월+28일 + 1달 = 3월 28일 → 채우면 좋을 테스트 케이스
포인트
- 개념당 assert 문을 최소로
- 세트스 함수당 개념 하나만 테스트
F.I.R.S.T
- 이미지 from
1. FAST
- 테스트는 빨라야 한다
- 느리면 자주 못 돌림 → 초반에 문제 못 찾음, 코드 마음껏 정리 불가 → 코드 품질 망가짐
2. Isolates
- 각 테스트는 서로 의존하면 안된다
- 각 테스트는 독립적으로 어떤 순서로 실행해도 괜찮아야 한다
- 테스트가 의존하면? → 연쇄 실패 발생 → 원인 진단 어려움 → 테스트 결함 숨겨짐
3. Repeatable
- 테스트는 어떤 환경에서도 반복 가능해야 한다
- 실제 환경, QA 환경, 오프라인된 노트북에서도 실행 가능해야 한다
- 테스트가 돌아가지 않는 환경이 하나라도 있으면 안된다
4. Self-validating
- 테스트는 bool값으로 결과를 내야 한다
- 스스로 성공과 실패가 가늠해야 한다
- 그러지 않으면? → 주관적 판단 → 지루한 수작업 평가
4. Timely
- 테스트는 적시에 작성해야 한다
- 테스트 하려는 실제 코드 구현 직전에 구현한다
- 실제 코드 구현 후엔? 실제 코드가 테스트 하기 어렵다는 사실 발견
- 테스트가 불가능한 실제 코드 설계 했을 수도
결론
- ‘깨끗한 테스트 코드’는 책 한 권을 할애해도 모자랄 주제다
- 테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존/강화
→실제 구현 코드보다 더 중요할지도 - 테스트 코드는
- 깨끗하게 지속적으로 관리
- 표현력 높이고 간결하게 정리
- 테스트 API를 구현해 DSL을 만들어 테스트 코드를 짜기 쉽게 만들자
Related POST
- [Clean Code] Ⅰ. 깨끗한 코드
- [Clean Code] Ⅱ.의미 있는 이름
- [Clean Code] Ⅲ. 함수
- [Clean Code] Ⅳ. 주석
- [Clean Code] Ⅴ. 형식 맞추기
- [Clean Code] Ⅵ. 객체와 자료구조
- [Clean Code] Ⅶ. 오류 처리
- [Clean Code] Ⅷ. 경계
- [Clean Code] Ⅸ. 단위 테스트
- [Clean Code] Ⅹ. 클래스
- [Clean Code] Ⅺ. 시스템
- [Clean Code] Ⅻ. 창발성(創發性)
- [Clean Code] XIII. 동시성
- [Clean Code] XIV. 점진적 개선(SUCCESSIVE REFINEMENT)
- [Clean Code] XV. JUnit 들여다보기
- [Clean Code] XVI. SerialDate 리팩터링
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] 다 읽었다~