목차
좋은 테스트 코드를 만들기 위해 주의해야 할 사항
테스트코드와 유지보수
테스트 코드는 그 자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상이 된다.
실패한 테스트를 통과시키기 위해 많은 노력이 필요하면 점점 테스트 코드에서 멀지고 TDD에서도 멀어진다.
악순환 예시
- 테스트 코드를 유지보수하는데 시간이 많이 든다 → 실패하는 테스트 증가 →실패한 테스트에 무감각 → 깨지는 테스트를 방치(주석처리) → 테스트 실패 여부에 상관없이 빌드 배포
이를 막기위해
테스트 코드 자체의 유지 보수성이 좋아야한다.
좋은 테스트 코드를 만들기 위해 주의해야 할 사항
1. 변수나 필드를 사용해서 기댓값 표현하지 않기 → 실제 값을 사용하자
(예시) 기대하는 값에 변수를 사용한 코드
// 기대하는 값에 변수 사용
@Test
void dateFromat(){
LocalDate date = LocalDate.of(1945,8,15);
String dateStr = formatDate(date)
assertEquals(data.getYear() + "년 " +
date.getMonthValue() + "월 " +
date.getDayOfMonth() + "일 ", dateStr);
}
// 기대하는 값에 문자열 사용
...
assertEquals("1945년 8월 15일", dateStr);
...
- 변수를 사용하면 메서드를 잘 못 사용했을 때 테스트가 깨져야 실수를 알 수 있다.
- 문자열을 사용한 예시는 기대하는 값이 명확하다.
- 메서드를 잘 못 사용할 일 없음
- 테스트가 깨지면 formatDate() 메서드만 확인하면 해결
(예시) 단언과 객체 생성에 필드와 변수를 사용한 코드
private List<Integer> answers = Arrays.asList(1,2,3,4);
@Test
void arrayCheck(){
// 어레이 저장 코드 : answers 을 사용
// 저장 결과를 확인 : savedArray
assertALL(
() -> assertEquals(answers.get(0), savedArray.get(0))
() -> assertEquals(answers.get(1), savedArray.get(1))
() -> assertEquals(answers.get(2), savedArray.get(2))
() -> assertEquals(answers.get(2), savedArray.get(3))
}
// 개선한 코드
@Test
void arrayCheck(){
// 어레이 저장 코드 : Arrays.asList(1,2,3,4) 을 사용
// 저장 결과를 확인 : savedArray
assertALL(
() -> assertEquals(1, savedArray.get(0))
() -> assertEquals(2, savedArray.get(1))
() -> assertEquals(3, savedArray.get(2))
() -> assertEquals(4, savedArray.get(3))
}
첫 번째 테스트 코드 처럼 작성을 하면 메스드가 많은 경우
→ 테스트를 이해하기 위해서 객체와 변수를 확인하기 위해 필드와 변수를 계속 확인 해야한다
개선된 코드의 경우 실제 값을 사용
→ 가독성이 올라가고 생성된 객체와 변수를 확인하기 위해서 필드와 변수를 오갈 필요가 없다.
2. 두 개 이상을 검증하지 않기 → 검증 대상을 명확하게 구분 → 유지보수에 유리
하나의 테스트에 두 개 이상을 검증하면,
테스트에 실패 했을 때, 둘 중 무엇이 실패했는지 확인해야 한다.
→ 서로 다른 내용을 검증한다면(검증 대상이 명확하게 구분되다면),
각 검증을 대상을 별도로 분리 → 테스트의 집중도 향상, 유지보수에 유리
3. 정확하게 일치하는 값으로 모의 객체 설정하지 않기 → 범용적인 값을 사용
모의 객체는 가능하 범용적인 값을 사용
- 한정된 값에 일치하도록 모의객체를 사용? → 약간의 코드 수정만으로도 테스트 실패
- 이 경우 테스트 코드의 일부 값을 수정하면 모의 객체 관련 코드도 수정 필요
4. 과도하게 구현 검증하지 않기 → 실행 결과를 검증
테스트 대상의 내부 구현을 검증을 하면?
→ 내부 구현은 언제든지 바뀔수 있으므로 실행 결과를 검증해야한다.
(예) 테스트 대상이 "비밀번호의 강도"를 확인하는 코드면 → 실행 결과가 정확하게 약하다, 강하다를 반환하는지 검증하면 된다.
5. 셋업을 이용해서 중복된 상황을 설정하지 않기 → 상황 구성 코드가 테스트 메서드 안에 작성
테스트 코드를 작성하다 보면 각 테스트 코드에서 동일한 상황이 필요할 때가 있다.
이경우 중복된 코드를 제거하기 위해 → @BeforeEach 메서드 ( 상황설정 메서드)를 이용해서 상황을 구성할 수 있다.
중복을 제거하고 코드 길이도 짧아져서 코드 품질이 좋아졌다고 생각할 수 있지만, 테스트 코드에서는 상황이 달라진다.
메서드가 문제가 생겨서 몇 달만에 테스트 코드를 다시 본 경우
- @BeforeEach 에 설정된 상황?을 다시 확인해가면서 문제의 원인을 분석해야함 (위아래로 반복적으로 이동)
- 모든 테스트 메서드가 동일한 상황을 공유하므로 이전에 한 변경으로 인해 테스트가 실패할 수 있다.
각 테스트 메서드는 검증을 목표로 하는 하나의 완전한 프로그램이여야 한다
→ 검증 내용을 스스로 잘 설명할 수 있어야 한다
→ 상황 구성 코드가 테스트 메서드 안에 위치해야 한다.
▶ 통합 테스트에서 데이터 공유 주의하기 →
통합 테스트 메서드는 데이터 초기화를 위한 코드를 작성하지 않아도 된다.
(이유) 초기화를 위한 쿼리 파일을 조금만 변경해도 많은 테스트가 실패할 수 있다.
통합 테스트 코드를 만들 때는 다음의 두 가지로 초기화 데이터를 나눠서 생각
- 모든 테스트가 같은 값을 사용하는 데이터: 예) 코드값 데이터
- 테스트 메서드에서만 피룡한 데이터 : 예) 중복 ID 검사를 위한 회원 데이터
▶ 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기
각 테스트 메서드에서 직접 상황을 구성하다 보면 여러 테스트 코드에서 중복되는 경우가 있다.
이럴 때 상황 설정을 위한 클래스를 이용하면 편리하다.
public class UserGivenHelper{
...
public void givenUser(Stering id, String pw, String email){
// 코드
}
}
@BeforeEach
void setUp() {
given = new UserGivenHelper(jdbcTemplate);
}
@Test
void 동일ID가_이미_존재하면_익셉션() {
given.givenUser("cbk", "pw", "cbk@cbk.com");
// 실행, 결과 확인
assertThrows(DupIdException.class,
() -> register.register("cbk", "strongpw", "email@email.com")
);
}
6. 실행 환경이 다르다고 실패하지 않기 → 프로젝트 폴더 기준 절대경로 사용
실행 환경에 따라 문제가 되는 가장 전형적인 이유는 파일 경로 → 프로젝트 폴더를 기준으로 절대경로 를 이용하자.
7. 실행 시점이 다르다고 실패하지 않기
시간 문제 : (예시) 회원의 만료여부를 확인하는 기능
@Test
void notExpired() {
LocalDateTime expiry = LocalDateTime.of(2024,10,15,0,0,0);
Member m = Member.builder().expiryDate(expiry).build();
assertFalse(m.isExpired()); // false 를 반환해야함
}
- 2024.10.16일에 테스트하면 테스트 실패
해결책
- 코드에서 명시적으로 시간을 제어할 수 있는 방법을 선택
- 시간 클래스를 작성
@Test
void notExpired() {
LocalDateTime expiry = LocalDateTime.of(2024,10,15,0,0,0);
Member m = Member.builder().expiryDate(expiry).build();
assertFalse(m.passedExpiryDate(LocalDateTime.of(2024,10,14,0,0,0)));
}
명시적으로 시간을 제어하면 언제 테스트를 하던지 테스트가 성공
랜덤 값 문제 : (예시) 숫자 야구 게임
문제 : 테스트를 통과시킬 수 있는 값이 매번 바뀜 → 테스트는 랜덤하게 실패
해결법
- 직접 랜덤 값을 생성하지 말고, 생성자를 통해 값을 받거나 랜덤 값 생성을 다른 객체에게 위임하게 바꾼다.
8. 필요하지 않은 값은 설정하지 않기
- 검증할 내용에 관련없는 값들은 해당 테스트 메서드에 있을 필요가 없다.
- 테스트할 범위에 꼭 필요한 값들만 설정하면 테스트 코드도 짧아지고 가독성도 높아진다.
▶ 단위 테스트를 위한 객체 생성 보조 클래스
- 상황 구성을 위해 필요한 데이터가 다소 복잡한 경우 -> 테스트를 위한 객체 생성 클래스 구현하는 편이 좋다.
- 기본 값을 설정하고 변경하고 싶은 속성만 변경하도록 설정 가능해진다.
9. 조건부로 검증하지 않기
테스트는 성공하거나 실패해야한다.
조건을 단언하지 않으면 그 테스트는 성공하지도 실패하지도 않은 테스트가 된다.
→ 조건도 단언을 하도록 수정하여 실패인지 성공인지 여부를 테스트해야 한다.
10. 통합 테스트는 필요하지 않은 범위까지 연동하지 않기
- 통합 테스트 실행시에 전체 애플리케이션을 구동하면 필요하지 않은 객체까지 생성하게 되어 테스트 실행이 느려진다.
- 테스트에 필요한 연동 대상만 생성하면 테스트 수행 시간이 짧아진다.
11. 더 이상 쓸모 없는 테스트 코드 → 제거
- 소프트웨어가 제공해야 하는 기능을 테스트하는 코드가 아니라면 삭제한다.
- 특정 클래스의 사용법을 익히기 위해 작성한 테스트 코드 → 삭제
- 단지 테스트 커버리지를 높이기 위한 목적으로 작성한 테스트 코드는 실제 코드 유지보수에는 아무런 도움이 되지 않으므로 삭제한다.
- User 클래스에 있는 단순한 메서드( getId, getName, ..) → 삭제