본문 바로가기

Spring/Test

TDD, 테스트 코드의 구성

1. 상황 찾기

어떤 상황이 실행 결과에 영향을 줄 수 있는지 찾기 위한 노력 필요

다양한 예외 상황을 찾아내고 이를 코드에 반영해야 기능이 비정상적으로 동작하는 것을 막을 수 있음

 

2. 테스트 코드의 구성 요소

기능은 상황에 따라 달라 지기 때문에  상황(given), 실행(when) 결과 확인(then) 세 가지 요소로 테스트를 구성할 수 있음

JUnit에서 상황을 설정하는 방법은 테스트할 대상에 따라 달라짐

 

1) 각 테스트 메서드마다 객체를 생성해서 상황 설정

public class BaseballGameTest {

    @Test
    void exactMatch() {
        // given
        BaseballGame game = new BaseballGame("456");

        // when
        Score score = game.guess("456");

        // then
        assertEquals(3, score.strikes());
        assertEquals(0,score.balls());
    }

    @Test
    void noMatch() {
        // given
        BaseballGame game = new BaseballGame("123");

        // when
        Score score = game.guess("456");

        // then
        assertEquals(0, score.strikes());
        assertEquals(0,score.balls());
    }
}

 

2) @BeforeEach를 적용한 메소드에서 상황 설정(주로 상황 설정과 관련된 대상을 필드로 보관)

public class BaseballGameTest {

    @BeforeEach
    void givenGame() {
        game = new BaseballGame("456");
    }

    @Test
    void exactMatch() {

        // when
        Score score = game.guess("456");

        // then
        assertEquals(3, score.strikes());
        assertEquals(0, score.balls());

    }

}

 

3) 상황이 없는 경우, 결과에 영향을 주는 상황이 존재하지 않을 때

 

- 기능을 실행하고 결과를 리턴 값으로 확인

@Test
void meetsAllCriteria_Then_Strong() {
    // when
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter("ab12!@AB");
    
    // then
    asserEquals(PasswordStrength.STRONG, result);
}

 

- 실행 결과가 리턴 값이 아닌 예외가 발생하는 것이 정상 실행 결과인 경우

@Test
void genGame_With_DupNumber_Then_Fail() {
	assertThrows(IllegalArgumentException.class,
		() -> new BaseballGame("110")
	);
}

 

📝 상황(given), 실행(when) 결과 확인(then) 구조에 너무 집착하지 말기! 테스트 코드를 작성하는데 도움이 되지만 꼭 모든 테스트 메소드를 이 구조로 만들어야 하는 것은 아님, 테스트 코드를 보고 테스트 내용을 이해할 수 있으면 됨

 

 

3. 외부 상황과 외부 결과

상황 설정은 테스트 대상만 있는 것이 아닌 외부 요인도 상황 설정에 고려 대상임

 

파일이 필요한 테스트는 파일이 존재 여부에 따른 상황의 결과 확인도 필요함

file dataFile = new File("file.txt");
long sum = MathUtils.sum(dafaFile);

 

 

1) 파일이 존재하지 않는 경우

- 존재하지 않는 파일을 경로로 사용하는 방법이 있지만 항상 테스트 성공이 보장되지 않음, 파일이 우연히 존재한다면? 상황에 따라 결과가 달라진다면 테스트는 신뢰할 수 없음

 

-  명시적으로 파일이 없는 상황을 만들어서 테스트 , 해당 경로에 파일이 존재하는지 검사해서 존재할 경우 파일 삭제

 @Test
    void noDataFile_Then_Exception() {
        givenNoFile("badpath.txt");
        
        File dataFile = new File("badpath.txt");
        assertThrows(IllegalArgumentException.class,
                () -> MathUtils.sum(dataFile));
    }
    
    private void givenNoFile(String path) {
        File file = new File(path);
        if (file.exists()) {
            boolean deleted = file.delete();
            if (!deleted)
                throw new RuntimeException("file givenNoFile: " + path);
        }
    }

 

 

2) 파일이 존재하는 경우

- 상황에 알맞은 파일을 src/test/resource 폴더에 미리 만들어 두는 방법, 주의할 점은 다른 개발자들도 테스트가 가능하도록 테스트에 맞게 준비한 파일을 버전 관리 대상에 추가해야함

 

- 파일을 미리 만들지 않고 테스트 코드에서 상황에 맞는 파일을 생성하는 방법

 @Test
    void dataFileSumTest() {
        givenDataFile("target/datafile.txt", "1", "2", "3", "4");
        File dataFile = new File("target/datafile.txt");
        long sum = MathUtils.sum(dataFile);
        assertEquals(10L, sum);
    }
    
    private void givenDataFile(String path, String ... lines) {
        try {
            Path dataPath = Paths.get(path);
            if (Files.exists(dataPath)) {
                Files.delete(dataPath);
            }
            Files.write(dataPath, Arrays.asList(lines));            
        } catch (IOException e) {
            throw new RuntimeException((e));
        }
        
    }

 

 

4. 외부 상태가 테스트 결과에 영향을 주지 않게 하기

DB 데이터 상태(외부 상태) 에 따라 테스트 결과가 달라지는 경우 

예를 들어 회원가입 기능에서 중복된 아이디로 가입 시 가입이 안되는 기능을 테스트 하는 경우, DB에 회원 테이블에 아이디가 없는 경우와 아이디가 있는 경우 테스트 결과가 달라 질 수 있음

 

1) 테스트 실행 전 외부 상태를 원하는 상태로 만들기: 테스트 실행 전 테스트하려는 아이디를 삭제하는 로직 추가

 

2) 테스트 실행 후 외부 상태를 원래대로 돌려 놓기: 테스트 메소드 실행 후 트랜잭션을 롤백

 

 

5. 외부 상태와 테스트 어려움

상황과 결과에 영향을 주는 외부 요인은 파일, DBMS, 외부 서버(외부 REST API 사용) 등 다양

외부 환경을 테스트에 맞게 구성하는 것이 항상 가능 한 것은 아니기 때문에 대역을 사용하여 테스트 코드 작성에 도움을 받을 수 있음

 

 

 

 

참고: 최범균의 테스트 주도 개발(TDD) 시작하기