목차

좋은 테스트 코드를 만들기 위해 주의해야 할 사항

테스트코드와 유지보수

테스트 코드는 그 자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상이 된다.

실패한 테스트를 통과시키기 위해 많은 노력이 필요하면 점점 테스트 코드에서 멀지고 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, ..)  → 삭제

+ Recent posts