반응형
TDD 이전의 개발
- 만들 기능에 대해 설계와 구현에 대해서 고민
- 기능에 대한 구현을 완료한 것 같으면 기능을 테스트
TDD란?
- 기능을 검증하는 테스트 코드를 먼저 작성하고 테스트를 통과시키기 위해 개발을 진행
- 테스트를 먼저 한다는 것은 기능이 올바르게 동작하는지 검증하는 테스트 코드를 작성한다는 것을 의미
- TDD를 접할 때는 작은 단계를 차근차근 밟아 나가야 함
TDD 흐름
- 기능을 검증하는 테스트를 먼저 작성
- 테스트를 통과하지 못하면 테스트를 통과할 만큼만 코드를 작성
- 테스트 통과 후 개선할 코드가 있으면 리팩터링
- 테스트 코드를 만들면 다음 개발 범위가 정해짐
- 테스트 코드가 추가되면서 검증하는 범위가 넓어질수록 구현도 점진적으로 완성
- TDD의 장점은 코드 수정에 대한 빠른 피드백이며 잘못된 코드가 배포되는 것을 방지
테스트 코드 작성 순서
- 쉬운 경우에서 어려운 경우로 진행
- 예외적인 경우에서 정상적인 경우로 진행
- 가장 쉬운 경우부터 시작해야 빠르게 테스트를 통과시킬 수 있음
- 한 번에 구현하는 시간이 짧아지면 디버깅하는 시간도 짧아짐
- 예외 상황을 먼저 찾고 테스트에 반영하면 예외 상황을 처리하지 않아 발생하는 버그를 줄여줌
- TDD 학습 시 도움이 되는 단계
- 정해진 값을 리턴
- 값 비교를 이용해서 정해진 값을 리턴
- 다양한 테스트를 추가하면서 구현을 일반화
- 테스트 시작이 안될 때는 검증문부터 작성하면 도움이 됨
TDD 기능 명세 설계
- 설계는 기능 명세로부터 시작
- 기능 명세를 구체화하는 동안 입력과 결과를 도출하고 코드에 반영
- 입력과 결과를 코드에 반영하는 과정에서 기능의 이름, 파라미터, 리턴 타입 등이 결정
- 타입의 이름을 정의하고, 타입이 제공할 기능을 결정하는 것은 기본적인 설계 행위
- TDD 자체가 설계는 아니지만 테스트 코드를 작성하는 과정에서 일부 설계를 진행하게 됨
- TDD로 개발을 진행하면 현시점에서 테스트를 통과시키는데 필요한 만큼의 코드만 구현하며 유연한 설계는 필요한 시점에 추가하는 것
- 기능 명세가 모호한 상황이면 구체적인 예로 바꾸어 테스트 코드에 반영해야 함
Junit 5 기초
- 구성
- JUnit 플랫폼: 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공
- JUnit 주피터(Jupiter): JUnit 5를 위한 테스트 API와 실행 엔진을 제공
- JUnit 빈티지(Vintage): JUnit 3과 4로 작성된 테스트를 JUnit5 플랫폼에서 실행하기 위한 모듈을 제공
- JUnit 기본 구조
- 테스트를 진행할 메서드에 @Test 애노테이션을 붙이며 private이면 안 됨
- 클래스 명의 접미사에 'Test' 접미사를 붙임
- 주요 단언 메서드
- assertEquals(expected, actual)
- 실제 값(actual)이 기대하는 값(expected)과 같은지 검사
- assertNotNull(unexpected, actual)
- 실제 값(actual)이 특정 값(unexpected)과 같지 않은지 검사
- assertSame(Object expected, Object actual)
- 두 객체가 동일한 객체인지 검사
- assertNotSame(Object unexpected, Object actual)
- 두 객체가 동일하지 않은 객체인지 검사
- assertTrue(boolean condition)
- 값이 true인지 검사
- assertFalse(boolean condition)
- 값이 false인지 검사
- assertNull(Object actual)
- 값이 null인지 검사
- assertNotNull(Object actual)
- 값이 null이 아닌지 검사
- fail()
- 테스트를 실패 처리
- assertThrows(Class<T> expectedType, Executable executable)
- executable을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사
- assertDoesNotThrow(Executable executable)
- executable을 실행한 결과로 익셉션이 발생하지 않는지 검사
- assertEquals(expected, actual)
- 테스트 라이프사이클
- JUnit은 각 테스트 메서드마다 다음 순서대로 코드를 실행
- 테스트 메서드를 포함한 객체 생성
- (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
- @Test 애노테이션이 붙은 메서드 실행
- (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행
- @BeforeEach는 테스트를 실행하는데 필요한 준비 작업을 할 때 사용
- @AfterEach는 테스트를 실행한 후에 정리할 것이 있을 때 사용
- @BeforeAll은 클래스의 모든 테스트 메서드를 실행하기 전에 한 번 실행
- @AfterAll은 클래스의 모든 테스트 메서드를 실행한 뒤에 실행
- 각 테스트는 상호 독립적이어야 하고 실행 순서에 의존하지 않으며 필드를 공유하면 안 됨
- @DisplayName은 테스트의 표시 이름을 붙임
- @Disable은 테스트 실행 대상에서 제외시킬 때 사용
- JUnit은 각 테스트 메서드마다 다음 순서대로 코드를 실행
테스트 코드의 구성
- 기능은 상황에 따라 결과가 달라짐
- 어떤 상황이 주어지고, 그 상황에서 기능을 실행하고, 실행한 결과를 확인하는 세 가지가 테스트 코드의 기본 골격
- 실행 결과가 항상 리턴 값으로 존재하지 않는 경우도 있음 (예: 예외 발생)
- 상황과 결과에 영향을 주는 외부 요인은 파일, DBMS, 외부 서버 등 다양하며 외부 환경을 테스트에 맞게 구성하는 것이 항상 가능하지는 않음
- 테스트 대상이 아닌 외부 요인은 테스트 코드에서 다루기 힘들며 테스트 코드에서 마음대로 제어할 수 없는 경우가 있음
- 테스트 코드에서 생성한 외부 결과를 마음대로 초기화하기 힘들 때도 있음
- 대역은 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인데 이를 통해서 외부 상황이나 결과를 대체 가능
대역
- 외부 요인은 테스트 작성을 어렵게 만들 뿐만 아니라 테스트 결과도 예측할 수 없게 만듦
- 외부 요인 때문에 테스트가 어려울 때는 대역을 써서 테스트를 진행
- 대역의 종류
- 스텁(stub)
- 구현을 단순한 것으로 대체
- 테스트에 맞게 단순히 원하는 동작을 수행
- 가짜(Fake)
- 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공
- 스파이(Spy)
- 호출된 내역을 기록하며 테스트 결과를 검증할 때 사용
- 모의(Mock)
- 기대한 대로 상호작용하는지 행위를 검증하며 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있음
- 스텁(stub)
- 제어하기 힘든 외부 상황이 존재하면 다음과 같은 방법으로 의존을 도출하고 이를 대역으로 대신할 수 있음
- 제어하기 힘든 외부 상황을 별도 타입으로 분리
- 테스트 코드는 별도로 분리한 타입의 대역을 생성
- 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
- 대역을 이용해서 상황 구성
- 구현이 오래 걸리는 로직을 분리
- 결과 값을 확인하는 수단으로 모의 객체를 사용하기 시작하면 결과 검증 코드가 길어지고 복잡해짐
- 모의 객체는 메서드 호출 여부를 검증하는 수단으로 테스트 대상과 모의 객체 간의 상호 작용이 조금만 변경되어도 테스트가 깨지기 쉬움
- 모의 객체의 메서드 호출 여부를 결과 검증 수단으로 사용하는 것은 주의해야 하며 대안으로 가짜 대역을 구현해서 테스트하는 방법이 있음
테스트 가능한 설계
- 테스트가 어려운 코드는 다음과 같음
- 하드 코딩된 경로
- 의존 객체를 직접 생성
- 정적 메서드 사용
- 실행 시점에 따라 달라지는 결과
- 역할이 섞여 있는 코드
- 메서드 중간에 소켓 통신 코드가 포함
- 콘솔에서 입력을 받거나 결과를 콘솔에 출력
- 테스트 대상이 사용하는 의존 대상 클래스나 메서드가 final
- 테스트 대상의 소스를 소유하고 있지 않은 경우
- 테스트가 어려운 주된 이유는 의존하는 코드를 교체할 수 있는 수단이 없기 때문
- 테스트 가능한 설계는 다음과 같음
- 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
- 의존 대상을 주입 받기
- 의존 대상은 주입 받을 수 있는 수단을 제공해서 교체할 수 있도록 해야 함
- 테스트하고 싶은 코드를 분리
- 시간이나 임의 값 생성 기능 분리
- 외부 라이브러리는 직접 사용하지 말고 감싸서 사용
- 외부 라이브러리를 대체하기 어렵다면 외부 라이브러리와 연동하기 위한 타입을 따로 만듦
테스트 범위와 종류
- 기능 테스트(Functional Testing)
- 사용자 입장에서 시스템이 제공하는 기능이 올바르게 동작하는지 확인
- 이 테스트를 수행하려면 시스템을 구동하고 사용하는데 필요한 모든 구성요소가 필요
- 끝(브라우저)에서 끝(데이터베이스)까지 올바른지 검사하기 때문에 E2E(End to End) 테스트
- 통합 테스트(Integration Testing)
- 시스템의 각 구성 요소가 올바르게 연동되는지 확인
- 소프트웨어 코드를 직접 테스트
- 단위 테스트(Unit Testing)
- 개별 코드나 컴포넌트가 기대한대로 동작하는지 확인
- 한 클래스나 한 메서드와 같은 작은 범위를 테스트
- 통합 테스트는 기능 데스트에 비해 제약이 덜하며 시스템의 내부 구성 요소에 대한 테스트도 가능
- 단위 테스트는 통합 테스트로도 만들기 힘든 상황을 쉽게 구성할 수 있음
- 테스트 속도는 통합 테스트보다 단위 테스트가 빠르기 때문에 가능하면 단위 테스트에서 다양한 상황을 다루고,
통합 테스트나 기능 테스트는 주요 상황에 초점을 맞춰야 함
테스트 코드와 유지보수
- 테스트 코드는 그 자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상
- 테스트를 유지보수하지 않을 경우 발생하는 문제
- 실패한 테스트가 새로 발생해도 무감각해짐
- 테스트 실패 여부에 상관없이 빌드하고 배포
- 빌드를 통과시키기 위해 실패한 테스트를 주석 처리 후 고치지 않음
- 테스트 유지보수성이 떨어지지 않게 하려면 아래와 같이 테스트 코드를 작성해야 함
- 변수나 필드를 사용해서 기대값 표현하지 않기
- 두 개 이상을 검증하지 않기
- 정확하게 일치하는 값으로 모의 객체 설정하지 않기
- 모의 객체는 가능한 범용적인 값을 사용해서 기술해야 함
- 한정된 값에 일치하도록 모의 객체를 사용하면 약간의 코드 수정만으로도 테스트는 실패하게 됨
- 과도하게 구현 검증하지 않기
- 내부 구현을 조금만 변경해도 테스트가 깨질 가능성이 있음
- 셋업을 이용해서 중복된 상황을 설정하지 않기
- 테스트 메서드는 자체적으로 검증하는 내용을 완전히 기술하고 있어야 테스트 코드를 유지보수하는 노력을 줄일 수 있음
- 통합 테스트에서 데이터 공유 주의하기
- 모든 테스트가 같은 값을 사용하는 데이터와 특정 테스트에서만 필요한 데이터를 나눠서 생각해야 함
- 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기
- 실행 환경이 다르다고 실패하면 안 됨
- @EnabledOnOs, @DisabledOnOs 애노테이션을 사용해서 OS에 따른 테스트 실행 여부를 지정
- 실행 시점이 다르다고 실패하지 않기
- 랜덤하게 실패하지 않기
- 필요하지 않은 값은 설정하지 않기
- 테스트 대상의 필수 속성이 많다면 테스트를 위한 객체 생성 보조 클래스 사용하기
- 조건부로 검증하지 않기
- 통합 테스트는 필요한 범위까지만 연동하기
- 학습 테스트 코드 및 테스트 커버리지를 높이기 위한 테스트 코드는 장기적으로 유지할 필요 없음
반응형
'책 소개' 카테고리의 다른 글
[요약] 자바 ORM 표준 JPA 프로그래밍 (0) | 2024.07.11 |
---|---|
[요약] 자바 8 인 액션 (0) | 2024.06.30 |
[요약] 도메인 주도 개발 시작하기 (0) | 2024.06.22 |
마음챙김 (0) | 2023.11.07 |
오펜하이머 각본집 (1) | 2023.10.29 |