읽게 된 동기
처음으로 프로젝트에서 테스트 코드를 작성해보면서 관련 책을 읽으려고 찾아봤다. 그 중에서 이 책이 제목은 TDD에 관한 제목이지만, 목차를 봤을 때 꼭 TDD가 아니더라도 테스트 코드 작성을 처음 해보려는 사람들이 읽기에 좋아 보였다. (그리고 실제로 그렇다) TDD도 역시 궁금했고, 테스트 코드에 대한 기초 내용도 알고 싶어서 읽게 되었다. 그리고 내가 일단 테스트 코드를 작성하곤 있지만.. 잘 작성하고 있는 건지, 어떻게 작성하는 게 맞는지 확인해보고 싶었다.
느낀점
테스트 코드 작성을 시작하지 얼마 안된 사람으로써, 내가 잘 작성하고 있던 부분과 잘못 사용하고 있던 부분을 딱 알 수 있었고, 더 좋은 방법을 알아갈 수 있었다.
예를 들어 나는 테스트 코드에서 기댓값을 거의 변수로 사용했었다. 그리고 책에선 필드나 변수 말고 직접 작성하라고 했다. 난 왜 거의 변수로 활용했었냐면, 그냥 왠지 중복된 값을 여러 곳에 적는 것도 그렇고, 나중에 값을 바꿔서 테스트해볼 수도 있지 않을까?라는 생각 때문이었다. 근데 생각해보면 테스트 코드를 작성 후 그 값을 변경할 일이 없었다. 그리고 책에 나온 내용과 아주 유사하게 배열에 있는 여러 값을 비교했어야됐는데, 배열 인덱스를 잘못 쓰는 경우를 똑같이 겪었었다... 이젠 기댓값을 직접 쓰도록 작성하면서 비교해봐야겠다.
테스트 시 상황 설정이나 객체 생성을 위한 보조 클래스로 빌더같은 걸 만들어서 활용하는 것도 좋아보인다. 이미 결과 검증같은 건 테스트용 보조 클래스를 만들어서 쓰고 있었는데, 테스트 코드도 역시 코드라는 생각을 계속 하면서 리팩터링하는 것이 중요해보인다. 솔직히 실제 기능 코드보다 테스트 코드를 작성할 때 좀 덜 생각했던 부분도 있는 것 같다. 하지만 역시 테스트 코드도 계속 리팩터링하고 깨끗하게 유지하도록 해야겠다.
또 아직 필요는 없었지만 '이런 경우에 어떻게 테스트하지?'라고 가끔 궁금했던 부분들이 있다. 예를 들어 여러 대역에 대한 내용들이다. 나중에 굉장히 유용하게 활용할 것 같다.
TDD에 관해선, TDD로 개발하면 객체의 내부 구현을 먼저 생각하는 게 아니고 클라이언트 관점에서 먼저 생각하는 게 좋은 것 같다. 또 불필요한 것까지 구현하거나, 여러 상황을 한꺼번에 생각하느라 아무것도 하지 못하는 상황을 방지할 수 있을 것 같다. 아직 테스트코드를 먼저 작성한 적은 없지만.. 다음 프로젝트에 꼭 적용해보고 싶다.
결론
TDD 뿐만 아니라 테스트 코드의 기초를 잡기에 굉장히 좋다! 또 테스트 코드를 작성은 하고 있지만 잘 작성하는 지 잘모르겠을 때 읽기에도 아주 좋아보인다. 책이 굉장히 얇고 술술 읽히는 편이다.
챕터별 정리
처음엔 간단한 예시에 대해 TDD로 개발을 해나가고, 그 후 예시를 왜 그런 순서로 테스트했는지를 알아본다. 그리고 JUnit5에 대해 훑어보고, 테스트를 더 잘하기 위한 것들에 대해 알아본다. 난 주로 AssertJ를 이용했었는데, AssertJ 소개도 마지막에 잠깐 나왔다.
Chapter 02. TDD 시작
TDD
TDD는 기능을 테스트하는 걸 먼저 만들고 나중에 구현을 한다.
- 이 과정에서 메서드 이름에 대해서도 고민하고, 파라미터의 개수와 타입, 반환 타입에대해서도 고민한다. 또 메서드를 정적 메서드로 구현할지 인스턴스 메서드로 구현할지, 메서드의 클래스 이름도 고민하게 된다. 테스트 만듦 → 테스트만 통과하도록 구현 → 그 테스트가 안통과하는 검증 코드 테스트에 추가 구현 위와 같이 점진적으로 구현을 완성한다.
테스트 코드도 코드다
테스트 코드도 코드이기 때문에 유지보수 대상이다. 즉 테스트 메서드에서 발생하는 중복을 알맞게 제거하거나 의미가 잘 드러나게 코드를 수정할 필요가 있다. → 솔직히 테스트 코드는 '테스트 코드'라고 생각해서 기능을 작성할 때보다 조금 덜 생각하고 작성했던 것 같아서 반성했다.
TDD 사이클: Red-Green-Refactor
- 기능을 검증하는 테스트를 작성
- 통과하지 못하면 통과할 만큼만 작성
- 테스트 통과 시 개선할 코드가 있으면 리팩토링
- 리팩토링 수행 후 다시 테스트를 실행해서 기능 망가졌는지 확인
TDD 장점
- 테스트 코드 덕에 리팩토링을 과감하게 가능 → 지속적으로 코드 정리를 하게 해주므로 코드 품질이 나빠지지 않게 막아준다. 향후 유지보수 비용을 낮추는데 기여한다.
- 빠른 피드백 → 잘못된 코드 배포 막아줌
Chapter 03. 테스트 코드 작성 순서
- 쉬운 경우에서 어려운 경우
- 예외적인 경우에서 정상인 경우
구현하기 쉬운 테스트부터 시작하기
- 한 번에 구현하는 시간이 짧아지면 디버깅할 때 유리하다. 작성한 코드가 많지 않고 작성 시간도 짧으면 머릿속에 코드에 대한 내용이 생생하게 남아있으므로 디버깅할 때 문제 원인을 빠르게 찾을 수 있다.
예외적인 것부터 테스트하기
- 예외 상황을 고려하지 않은 코드에 예외 상황을 반영하려면 코드의 구조를 뒤집거나 코드 중간에 예외 상황을 처리하기 위해 조건문을 중복해서 추가하는 일이 벌어진다. 이는 코드를 복잡하게 만들어 버그 발생 가능성을 높인다. 따라서 예외 상황을 먼저 테스트한다.
- 예외 상황을 먼저 테스트하면 예외 상황을 처리하지 않아 발생하는 버그도 줄여준다.
리팩토링 시점
테스트 통과 후엔 계속해서 리팩토링해나간다. 이 때 여러 리팩토링 방법이 있는데.. 상수 → 변수나 변수 이름 변경같은 건 바로 할 수 있지만, 메서드 추출과 같이 메서드 구조에 영향을 주는 리팩토링은 큰 틀에서 구현 흐름이 눈에 들어오기 시작한 뒤 진행하자. 초기부터 리팩토링을 진행하면 코드 구조를 잘못 잡을 가능성이 있다. 그럼 또 구현이 막힐 수 있다.
테스트 코드의 중복?
보통 중복을 제거하는 것이 좋지만 테스트 코드의 중복 제거는 고민이 필요하다. 각 테스트 메서드는 스스로 무엇을 테스트하는지 명확하게 설명할 수 있어야 하기 때문. 테스트 코드의 구현 중복을 기계적으로 제거하면 자칫 테스트 메서드가 검증하고 싶은 내용을 알아보기 힘들 수 있다. 테스트 코드에서 중복을 제거했다면 어떤 것을 검증하는 지 쉽게 확인할 수 있는지 확인해보고 아니라면 되돌리자.
인수 개수
메서드에 파라미터를 하나씩 넘기다가 테스트를 진행하면서 파라미터가 늘어났다. 만약 세개로 늘어났다고 해보자. 파라미터 개수는 적을 수록 코드 가독성과 유지보수에 유리하므로 메서드의 파라미터 개수가 세 개 이상이면 객체로 바꿔 한 개로 줄이는 것을 고려해봐야 한다.
테스트 To-do 목록 정리하기
TDD를 시작할 때 테스트할 목록을 미리 정리하면 좋다. 한 기능을 구현할 때 그 기능에 대해 테스트할 내용을 목록으로 정리하고 어떤 테스트가 구현이 쉬울지 상상해본다. 또는 어떤 테스트가 예외적인지 상상해본다. 또 테스트 과정에서 새로운 테스트 사례를 발견하면 그 사례를 목록에 추가해서 놓치지 않도록 하자. 또는 새로 발견한 테스트 사례를 실패하는 테스트로 등록해두는 것도 새로운 사례를 놓치지 않는 방법이 될 수 있다. 그렇다고 테스트 목록을 작성했다고 한번에 다 구현하면 안된다.
- 그럼 리팩토링을 마음껏 못하게 된다.
- 또 모든 테스트를 통과시키기 전까지 계속해서 깨지는 테스트가 존재하므로 개발 리듬이 깨진다. 한 테스트를 통과하고 리팩토링하는 과정이 반복되어야 한다. 만약 테스트를 하다가 범위가 큰 리팩토링을 발견하면 일단 리팩토링을 진행하지 말고 테스트 통과에 집중한다. 대신 할일 목록에 추가하자. 그리고 그런 리팩토링 전에 꼭 코드를 커밋하자.
Chapter 04. TDD 기능 명세 설계
어떤 형태든 기능을 구현하려면 기능을 크게 두 가지로 나눌 수 있다.
-
입력
- 보통 메서드의 파라미터
- 결과 - 리턴 값이나 예외 - 변경 - 예를 들어 회원 가입 기능은 실행 결과로 DB에 회원 정보를 추가한다. - 리턴 값으로는 알 수 없으므로 테스트 대상 실행 후에 변경 대상에 접근해서 결과를 확인해야 함. 설계는 기능 명세로부터 시작한다. 스토리보드를 포함한 다양한 형태의 요구사항 문서를 이용해서 기능 명세를 구체화한다. 구체화하는 동안 입력, 결과를 도출하고 이렇게 도출한 기능 명세를 코드에 반영한다.
필요한 만큼 설계하기
TDD는 테스트를 통과할 만큼만 작성한다. 필요할 것으로 예측해서 미리 코드를 만들지 않는다. 설계도 마찬가지다. 필요할 것으로 예측해서 미리 설계를 유연하게 만들지 않는다. TDD에서도 미리 앞서서 필요해 보이는 익셉션 타입을 만든다던가 하는 일을 하지 않는다. 이렇게 유연한 설계는 필요한 시점에 추가함으로써 설계가 불필요하게 복잡해지는 것을 방지할 수 있다.
Chapter 06. 테스트 코드의 구성
상황, 실행, 결과 확인
상황, 실행, 결과 확인의 세 가지 요소로 테스트 구성 가능
given when then
상황이 없을 때도 있고…
상황-실행-결과 확인 구조에 너무 집착하지는 말자. 테스트 코드를 작성하는 데 도움이 되는 것은 맞지만 꼭 모든 테스트 메서드를 이 구조로 만들 필요는 없다. 테스트 코드를 보고 테스트 내용을 이해할 수 있으면 된다.
외부 상태와 테스트 어려움
테스트 대상이 아닌 외부 요인(파일, DBMS, 외부 서버…)은 테스트 코드에서 다루기 힘들다. 이렇게 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우 대역을 사용하면 테스트 작성이 쉬워진다.
Chapter 07. 대역
Test Double
외부 요인에 의존하면 테스트를 작성하고 실행하기 어려워짐. 이럴 때는 대역을 써서 테스트 진행 가능
대역의 종류
대역 종류 | 설명 |
---|---|
스텁 Stub | 구현을 단순한 것으로 대체. 예를 들어 항상 true만 리턴하는 걸 만든다든가 |
가짜 Fake | 제품용으론 적합하진 않지만 실제 동작하는 구현 제공. 예를 들어 Repository 대신에 MemoryRepository로 그냥 맵에 원소를 저장하는 걸 만든다든가 |
스파이 Spy | 호출된 내역을 기록 해두고 있어서 검증할 때 사용 가능. 스텁이기도 함 |
모의 Mock | 기대하는 대로 상호작용하는 지 행위 검증 → 객체 간에 실제 메시지 전송이 기대대로 됐는지.. 기대한 대로 동작하지 않으면 익셉션 발생 가능 스텁이자 스파이 |
구현하기 전에 설계해보기
구현하기 전에 모든 기능을 설계하는 것은 불가능하다. 개발을 진행하는 도중에도 요구사항이 계속해서 바뀌기 때문. 그럼에도 불구하고 단위 기능을 구현 하기에 앞서 어떤 구성 요소가 필요할지 고민하는 것은 의존 대상을 도출할 때 도움이 된다. → 구현 전에 간단한 콜라보레이션 다이어그램등으로 고민해보는 것도 좋을 것 같다.
협업 대상 도출과 대역 사용
제어하기 힘든 외부 상황이 존재하면 다음과 같은 방법으로 의존을 도출하고 이를 대역으로 대신할 수 있다.
- 제어하기 힘든 외부 상황을 별도 타입으로 분리 ← 이걸
인터페이스
로 따로 만들어서 시작해보자! - 테스트 코드는 별도로 분리한 타입의 대역을 생성
- 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
- 대역을 이용해서 상황 구성
또는 당장 구현하는 데 시간이 오래 걸리는 로직도 분리하기에 좋은 후보이다.
모의 객체를 과하게 사용하지 않기
모의 객체를 이용하면 대역 클래스를 만들지 않아도 되니까 처음에는 편할 수 있지만, 결과 값을 확인하는 수단으로 사용하기 시작하면 결과 검증 코드가 길어지고 복잡해진다. 또한 모의 객체는 기본적으로 메서드 호출 여부를 검증하는 수단이므로 테스트 대상과 모의 객체 간의 상호작용이 조금만 바뀌어도 테스트가 깨지기 쉽다. → 모의 객체를 생각없이 사용하다보면 사실상 그 객체의 내부 구현 세부사항까지 의존하는거나 다름 없이 작성할 수도 있다. 따라서 Fake 같이 다른 대역을 사용하는 것도 생각해보자.
Chapter 08. 테스트 가능한 설계
결론적으로는 쉽게 교체할 수 있게 해야 한다! 테스트하기 어려운 경우들과 해결 방법을 알아보자.
-
하드 코딩된 경로
- 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
-
의존 객체를 직접 생성(주입이 아닌)
- 의존 대상을 주입 받기
-
실행 시점에 따라 달라지는 결과
- 예를 들어 시간이나 임의의 값 생성 기능 분리하기.
- 시간이나 임의의 값을 반환하는 클래스를 만들어 사용하고, 대역으로 테스트하기
-
역할이 섞여 있는 코드: 여러 로직이 섞여 있어 한 로직만 테스트할 수 없다든가…
- 테스트하고 싶은 코드 분리하기
-
메서드 중간에 소켓 통신 코드 포함되어 있을 때
- 소켓 통신이나 HTTP 통신은 실제를 대체할 서버를 로컬에 띄워서 처리하게 할 수도 있음. 즉 서버 수준에서 대역. (9장)
-
외부 라이브러리를 사용하여 테스트 대상의 소스를 소유하고 있지 않아 수정이 어려움 / 테스트 대상이 사용하는 의존 대상 클래스나 메서드가 final이라 대역으로 대체하기 어려움
- 두 경우 모두 직접 사용하지 말고 래퍼 클래스 만들어 감싸서 사용하기
Chapter 09. 테스트 범위와 종류
-
기능 테스트(E2E 테스트)
- 부득이한 경우를 제외하면 기능 테스트는 사용자와 동일한 방식으로 검증해야 함
-
통합 테스트
-
시스템의 각 구성 요소가 올바르게 연동되는지 확인
- 기능테스트: 사용자 입장에서 테스트
- 통합 테스트: 소프트웨어의 코드를 직접 테스트
-
-
단위 테스트
- 한 클래스, 한 메서드와 같은 작은 범위 테스트
- 의존 대상은 스텁이나 모의 객체 등을 이용해서 대역으로 대체
어떤 테스트를 해야 할까
상황 설정이나 테스트 시간 등의 이유로 자연스레 단위 테스트 코드를 더 많이 작성하게 된다. 그래도 통합 테스트는 필요함. 아무리 단위 테스트를 많이 만든다고 해도 결국은 각 구성 요소가 올바르게 연동되는 것을 확인해야 하는데 이를 자동화하기 좋은 수단이 통합 테스트 코드이기 때문이다. 테스트 속도는 단위 테스트가 빠르므로 가능하면 단위 테스트에서 다양한 상황을 다루고, 통합 테스트나 기능 테스트는 주요 상황에 초점을 맞춰야 한다. 그래야 테스트 실행 시간이 증가해 피드백이 느려지는 것을 방지할 수 있다.
Chapter 10. 테스트 코드와 유지 보수
테스트 코드 자체의 유지보수성이 좋아야 한다. 테스트 코드를 유지보수하기 좋아야 지속적으로 테스트를 작성하게 되고 결과적으로 소프트웨어의 품질이 떨어지는 것도 막을 수 있다. 유지보수하기 좋은 코드를 만들기 위해 필요한 좋은 패턴과 원칙이 존재하는 것처럼 좋은 테스트 코드를 만들려면 주의해야할 사항이 있다.
-
변수나 필드를 사용해서 기댓값 표현하지 않기
- 난 대부분 변수를 이용해서 기댓값을 표현했었다.. 중복되는 정보니까? 그리고 나중에 값을 바꾸더라도 한번에 바꿀 수 있으니까 변수로 관리하는 게 더 나을 거라고 생각했었는데.. 생각해보면 테스트에서 딱히 변수 값을 바꿀 일이 없기도 하고 그냥 테스트 메서드 내에서 한눈에 의미를 알아볼 수 있는 게 더 좋아 보이기도 한다. 이제 변수 말고 직접 작성해서 테스트해보고 비교해봐야겠다.
- 테스트 코드를 처음 보는 사람은(곧 미래의 내 자신도 이런 사람이 된다) 변수와 필드를 오가며 테스트 코드를 이해해야 한다.
- 기대값을 직접 작성하면 잘못된 변수를 작성해서 이유를 찾으라 고생할 필요도 없고, 객체 생성 시 사용한 값이 무엇인지 알아보기 위해 필드와 변수를 참조하지 않아도 된다. 또 단언할 때 사용한 값이 무엇인지 알기 위해 필드와 변수를 오갈 필요도 없다.
-
두 개 이상을 검증하지 않기
- 한 테스트에서 검증하는 내용이 두 개 이상이면 테스트 결과 확인 시 집중도가 떨어진다.
- 게다가 만약 첫 번째 검증에 실패하면 테스트는 거기서 멈춘다. 첫번째 검증 대상을 통과 시켜야 두번째 검증이 성공했는지 여부를 알 수 있다. 또 테스트에 실패하면 두 가지 검증 대상 중 무엇이 실패했는지 확인해야 한다.
- 물론 테스트 메서드가 반드시 한 가지만 검증해야 하는 것은 아니지만, 검증 대상이 명확하게 구분된다면 테스트 메서드도 구분하는 것이 유지보수에 유리하다.
-
정확하게 일치하는 값으로 모의 객체 설정하지 않기
- 테스트의 목적을 생각하자. 꼭 동작을 확인하는 것이지 어떤 값인지가 중요한 게 아니다.
any
등으로 작성하자. 모의 객체는 가능한 범용적인 값을 사용해서 기술해야 한다.
- 테스트의 목적을 생각하자. 꼭 동작을 확인하는 것이지 어떤 값인지가 중요한 게 아니다.
-
과도하게 구현 검증하지 않기
- 테스트 대상의 내부 구현을 검증하지 않도록 주의하자. 특히 모의 객체를 처음 사용할 때 특히 이런 유혹에 빠지기 쉽다. 결과적으로 테스트 코드 유지 보수에 도움이 되지 않는다.
-
내부 구현은 언제든지 바뀔 수 있기 때문에 테스트 코드는 내부 구현보다 실행 결과를 검증해야 한다.
- 언제든지 변하는 구현 말고 변하지 않는 인터페이스에 의존하자
-
만약 내부 구현을 꼭 테스트해야하는 경우가 온다면? 메소드를 나눠야 하는 경우가 아닌지 생각해보자.
- 기능이 정상적으로 동작하는지 확인할 수단이 구현 검증밖에 없다면 일단 모의 객체를 사용해서 테스트 코드를 작성하더라도 작성 후에는 점진적으로 코드를 리팩토링해서 구현이 아닌 결과를 검증할 수 있도록 시도해야 한다.
-
셋업을 이용해서 중복된 상황을 설정하지 않기
- 셋업을 이용하면 중복을 제거하고 코드 길이가 짧아져서 코드 품질이 좋아졌다고 생각할 수 있지만, 테스트코드에서는 상황이 달라진다.
- 예를 들어 테스트가 실패했을 때 상황을 분석하려면 셋업 메서드까지 다시 확인해야 한다. 또 셋업 메서드의 변경으로 인해 테스트가 깨지기 쉬워질 수도 있다.
- 테스트 메서드는 검증을 목표로 하는 하나의 완전한 프로그램이어야 한다. 따라서 각 테스트 메서드는 별도 프로그램으로서 검증 내용을 잘 설명할 수 있어야 한다. 그러기 위해서는 상황 구성 코드가 테스트 메서드 안에 위치해야 한다. 그래야 스스로 완전하게 테스트 내용을 설명할 수 있을 것! 코드가 다소 길어져도, 테스트 메서드 자체가 스스로를 잘 설명하는 것이 중요하다.
- 통합 테스트에서 데이터 공유 주의하기
-
통합 테스트의 상황 설정을 위한 보조 클래스 사용하기
- 셋업을 안쓰려고 하다보면 비슷한 중복 코드가 많아질 수 있다. 이럴 땐 보조 클래스를 만들어서 활용해보자.
- 예를 들어 매번 저장소에 User를 만들어야 된다고 해보자. 그럴 때 아래와 같이 작성할 수 있다.
public class UserGivenHelper { private UserRepository userRepository; public UserGivenHelper(UserRepository userRepository) { this.userRepository = userRepository; } public void givenUser(String id, String pw, String email) { userRepository.save(new User(id, pw, email)); } }
- 그럼 테스트 코드에선 아래와 같이 깔끔하게 상황 설정을 할 수 있다.
public class UserServiceTestWithHelper { private UserRepository userRepository; private UserGivenHelper given; @BeforeEach void setUp() { userRepository = new MemoryUserRepository(); } @Test void dupId() { // given given.givenUser("id", "pw", "email@email.com"); // when // then } }
- givenUser라는 이름을 통해 어떤 상황을 구성하는 지 알 수도 있고 각 테스트 메서드에서 상황 구성을 위한 코드 중복도 방지할 수 있다. - 마찬가지로 검증을 위해 데이터를 조회하는 코드도 여러 테스트 메서드에 중복되어 있으면 유지보수에 좋지 않다. 이럴 때도 위에서 상황 설정 보조 클래스를 만들었던 것처럼 결과 검증을 위한 보조 클래스를 만들어서 유지보수하기 좋게 만들어보자.
-
실행 환경/시점이 다르다고 실패하지 않기
- Junit5로 운영체제별로 테스트 코드를 작성할 수 있다.
-
랜덤하게 실패하지 않기
- 실행 시점이 다르거나, 랜덤값을 이용할 땐 시간/랜덤값을 인자로 받도록 메서드를 설정하거나 별도의 시간/랜덤값 생성 클래스를 작성해서 Fake, Mock 등으로 테스트하자.
-
필요하지 않은 값은 설정하지 않기
- 필요하지 않은 값을 설정하느라 고민할 필요도 없고 테스트 코드 파악에도 훨씬 좋다.
-
만약 모두 지정하지 않으면 안될 때는 Test용 Builder등으로 객체 생성 보조 클래스를 따로 만들어서 지정하지 않은 부분은 기본값으로 만들도록 활용해보자.
- 객체 생성 보조 클래스 - 팩토리 클래스나 빌더 패턴 이용하기
-
조건부로 검증하지 않기
- 반드시 테스트는 성공하거나 실패해야한다. 즉 단언을 실행해야 한다.
- if문 등으로 특정 상황에서만 단언문이 실행되도록 하지 말자. 그 if문의 조건까지 반드시 단언으로 만들자.
- 통합 테스트는 필요하지 않은 범위까지 연동하지 않기
Chapter 11. 마치며
테스트 코드의 효능(?)
TDD를 통해 테스트를 먼저 작성하면 구현이 올바른지 확인도 되고 심리적 안정을 준다. 테스트 코드는 회귀 테스트로 사용할 수 있기 때문에 새로운 기능을 추가했을 때 계속 확인할 수 있다.
TDD하면 개발 시간만 늘어나는거 아닌가요?
개발 시간은 크게 세 가지로 나눌 수 있다.
- 처음 코딩 시간
- 테스트 시간
- 디버깅 시간 테스트와 디버깅 시간도 개발 시간이다. 단순히 코딩만 한다고 개발 시간이 줄지는 않는다는 것. 테스트 시간을 줄이려면 테스트를 자동화 해야 함 → 테스트를 만들어야함 → TDD! 처음엔 개발 시간을 늘리는 것 같지만 뒤로 갈수록 반복되는 테스트 시간을 줄여주어 오히려 개발 시간을 줄여준다. 구현 코드와 테스트 시점간에 시간 간격이 커질 수록 원인 분석 시간을 테스트 코드를 통해 줄일 수 있다. 또 테스트 실패해도 원인을 바로 알 수 있으므로 디버깅 시간을 줄여준다.
맺음말
TDD는 마치 운동과도 같다. 운동을 꾸준히 하면 건강해지고 체력이 좋아지는 것처럼 TDD도 꾸준히 연습하고 적용해야 실력이 늘고 효과를 볼 수 있다. 초반에 힘들다고 바로 포기하면 오히려 안 하는 것만 못하다. 처음엔 쉬운 것부터 시작해서 익숙해질 때까지 갈고 닦고 습관이 될때까지 연습하고 또 연습하자!
부록C. Mockito 기초 사용법
-
given, willReturn, willThrow 가능
given().will..
- 만약 리턴 타입이 void라면
willThrow(Exception.class).given(모의 객체).메서드();
와 같이 사용한다. 인자로 Exception.class처럼 클래스나 객체를 받는다. -
스텁을 설정할 메서드의 인자가 두 개 이상인 경우 주의할 점
- 만약
(anyInt(). "123")
으로 한 인자라도 ArgumentMatcher를 통해서 설정할 경우 모두 matcher를 통해 설정해야 한다. - 따라서 위와 같은 경우
(anyInt(). eq("123"))
이렇게 작성해야함
- 만약
-
행위 검증
- 실제로 모의 객체가 불렸는지 검사 가능
then(모의객체).should().메서드(메서드인자)
- 만약 정확하게 한 번만 호출된 걸 검증하고 싶다면 should에
only()
넣을 것. 이 외에도 only times never atLeast atLeastOnce atMost(최대) 등이 있다.
부록D. AssertJ 소개
-
익셉션 검증
- 익셉션 발생
assertThatThrownBy
assertThatExceptionOfType(타입).isThrownBy(람다)
- 익셉션 발생X
assertThatCode(람다).doesNotThrowAnyException
으로 익셉션 발생하지 않을 것 검증 가능
-
한꺼번에 여러개 검증
- 원래 단언문을 여러개 적을 경우 위에서 실패하면 나머지는 실행되지 않는다. 이럴 때 여러개를 한꺼번에 검증할 수 있다.
SoftAssertions
로 모아서assertAll
로 한꺼번에 검증 가능SoftAssertions.assertSoftly
-
결과 메시지에 설명 달기:
as()
와describedAs()
assertThat(어쩌구).as(”ID 검사”).isEqualTo(저쩌구);
- 실패 메시지에 같이 출력된다.
- 한 테스트 메서드에서 다수의 검증 메서드 실행 시 유용하게 사용 가능