본문 바로가기
Engineering

[테스트 주도 개발 시작하기] 5 ~ 6장

by unknownomad 2023. 6. 14.

<5장> JUnit 5 기초

 

JUnit 5 구성요소

  • JUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API 제공
  • JUnit 주피터(Jupiter) : JUnit 5 를 위한 테스트 API 와 실행 엔진 제공
  • JUnit 빈티지(Vintage) : JUnit 3 & 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈 제공

 

@Test & 테스트 메서드

@Test

  • 테스트 실행할 메서드에 붙임
  • 해당 메서드는 private 이면 안됨

 

주요 단언 메서드

Assertions 클래스가 제공하는 주요 단언 메서드

메서드 설명
assertEquals(expected, actual) 실제 값(actual)이 기대하는 값(expected)과 같은지 검사
assertNotEquals(unexpected, 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() 테스트를 실패 처리함
try {
    AuthService authService = new AuthService();
    authService.authenticate(null, null);
    fail(); // 이 지점에 다다르면 fail() 메서드는 테스트 실패 에러 발생
} catch(IllegalArgumentException e) {
}

 

Assertions 가 제공하는 익셉션 발생 유무 검사 메서드

메서드 설명
assertThrows(Class<T> expectedType, Executable executable) executable 을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사
assertDoesNotThrow(Executable executable) executable 을 실행한 결과로 익셉션이 발생하지 않는지 검사
assertThrows(IllegalArgumentException.class,
    () -> {
        AuthService authService = new AuthService();
        authService.authenticate(null, null);
    }
});

assertThrows()

  • 발생한 익셉션 객체 리턴
  • 발생한 익셉션을 이용 후 추가 검증이 필요하면 assertThrows() 메서드가 리턴한 익셉션 객체를 사용하면 됨
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
    () -> {
        AuthService authService = new AuthService();
        authService.authenticate(null, null);
    }
});
assertTrue(thrown.getMessage().contains("id"));

Executable 인터페이스

  • assertThrows() & assertDoesNotThrow() 에 사용
  • execute() 메서드를 가진 함수형 인터페이스
public interface Executable {
    void execute() throws Throwable;
}

 

assert 메서드

  • 실패 시 다음 코드 실행하지 않고 바로 익셉션(AssertionFailedError) 발생

 

assertAll()

  • 모든 검증 실행 후 그중 실패한 것이 있는지 확인하고 싶을 때 사용
assertAll(
    () -> assertEquals(3, 5 / 2),
    () -> assertEquals(4, 2 * 2),
    () -> assertEquals(6, 11 / 2)
);
  • Executable 목록을 가변 인자로 전달받아 각 Executable 실행
  • 실행 결과로 검증에 실패한 코드가 있으면 그 목록을 모아 에러 메시지로 보여줌

테스트 라이프 사이클

@BeforeEach & @AfterEach

JUnit 을 활용한 각 테스트 메서드의 실행 순서

  1. 테스트 메서드를 포함한 객체 생성
  2. (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
  3. @Test 애노테이션이 붙은 메서드 실행
  4. (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행
public class LifecycleTest {
    public LifecycleTest() {
        System.out.println("new LifecycleTest");
    }
    
    @BeforeEach
    void setUp() {
        System.out.println("setUp");
    }
    
    @Test
    void a() {
        System.out.println("A");
    }
    
    @Test
    void b() {
        System.out.println("B");
    }
    
    @AfterEach
    void tearDown() {
        System.out.println("tearDown");
    }
}

=== result ===
new LifecycleTest
setUp
A
tearDown
new LifecycleTest
setUp
B
tearDown
  1. @Test 메서드 실행 때마다 객체 새로 생성
  2. @BeforeEach
  3. @Test
  4. @AfterEach
@BeforeEach @AfterEach
테스트 실행하는데 필요한 준비 작업 시 사용 테스트 실행 후 정리할 것이 있을 때 사용
테스트에서 사용할 임시 파일 생성 or 테스트 메서드에서 사용할 객체 생성 테스트에서 사용한 임시 파일을 삭제해야 할 때 사용
@Test 애노테이션과 마찬가지로, 해당 애노테이션이 붙은 메서드는 private 이면 안 됨

 

@BeforeAll & @AfterAll

@BeforeAll @AfterAll
한 클래스의 모든 테스트 메서드가 실행되기 전,
특정 작업을 수행해야 할 때 사용
클래스의 모든 테스트 메서드를 실행한 뒤 실행됨
둘 다 정적 메서드에 붙이는데, 클래스의 모든 테스트 메서드를 실행하기 전 or 후에 한 번 실행됨

 

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

public class BadTest {
    
    private FileOperator op = new FileOperator();
    private static File file; // 두 테스트가 데이터를 공유할 목적으로 필드 사용
    
    @Test
    void fileCreationTest() {
        File createdFile = op.createFile();
        assertTrue(createdFile.length() > 0);
        this.file = createdFile;
    }
    
    @Test
    void readFileTest() {
        long data = op.readData(file);
        assertTrue(data > 0);
    }
}
  • fileCreationTest() 에서 생성한 File 을 보관 ➡️ file 필드를 readFileTest() 에 사용
  • 테스트 메서드 실행할 때마다 객체를 새로 생성하기에, file 은 정적 필드로 정의
  • 여기선 fileCreationTest() 가 readFileTest() 보다 먼저 실행되는 것을 가정함

 

테스트 메서드 실행 순서에 대한 고찰

  • 테스트 메서드가 특정 순서대로 실행된다는 가정 하에 테스트 메서드를 작성하면 안 됨
  • JUnit 이 테스트 순서를 결정하긴 하나, 그 순서는 버전에 따라 달라질 수 있음
  • 위 예시에서 순서가 달라지면 테스트는 실패
  • ➡️ 각 테스트 메서드는 서로 독립적으로 동작해야 함
  • 한 테스트 메서드의 결과에 따라 다른 테스트 메서드의 실행 결과가 달라지면 안 됨
  • 테스트 메서드가 서로 필드를 공유하거나 실행 순서를 가정한 테스트를 작성해서는 안 됨

 

@DisplayName

  • 테스트에 표시 이름 붙이기
  • 자바는 메서드 이름에 공백이나 특수 문자를 사용할 수 없기에, 메서드 이름만으로 테스트 내용을 충분히 설명하기 어려울 수 있음
@DisplayName("@DisplayName 테스트")
public class DisplayNameTest {
    
    @DisplayName("값 같은지 비교")
    @Test
    void assertEqualsMethod() {
        ...
    }
}

 

@Disabled

  • 특정 테스트를 실행하고 싶지 않을 때 사용
  • JUnit 은 @Disabled 붙은 클래스나 메서드는 테스트 실행 대상에서 제외함
  • 테스트 코드가 미완성이거나 잠시 테스트를 실행하지 말아야 할 때 사용

 

모든 테스트 실행

  • 개발할 때는 특정 테스트 클래스나 메서드만 실행
  • ➡️ 원격 repo 에 푸시하거나 코드를 빌드해서 운영 환경에 배포하기 전, 모든 테스트를 실행해 깨지는 테스트가 없는지 확인
메이븐(Maven) 그레이들(Gradle)
mvn test (래퍼 사용한다면 mvnw test) gradle test (래퍼 사용한다면 gradlew test)
메이븐이 제공하는 라이프사이클에 따르면, package 단계 실행 시 test 단계를 앞서 실행하므로 mvn package 명령어를 실행해도 테스트 실행 그레이들도 build 테스크 실행 시 테스트 실행하므로, gradle build 명령어 실행하면 테스트 실행

 


 

<6장> 테스트 코드의 구성

 

상황 찾기

  • 어떤 상황이 실행 결과에 영향을 줄 수 있는지 알아야
  • 가능한 많은 예외 상황 찾기
  • 소프트웨어 품질 향상에 중요

 

테스트 코드 구성 요소

  • 상황 : given
  • 실행 : when
  • 결과 확인 : then

 

e.g. 1. 각 테스트 메서드마다 객체 생성해 상황 설정하기

@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());
}

 

e.g. 2. @BeforeEach 적용해 상황 설정하기

private BaseballGame game;

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

@Test
void exactMatch() {
    // given
    
    // when
    Score score = game.guess("456");
    
    // then
    assertEquals(3, score.strikes());
    assertEquals(0, score.balls());
}

 

e.g. 3. 상황이 없는 경우

  • 결과에 영향을 주는 상황이 존재하지 않는 경우, 아래처럼 기능 실행 후 결 확인하는 코드만 포함하기도 함
@Test
void meetsAllCriteria_Then_Strong() {
    // given
    
    // when
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter("ab12!2AB");
    
    // then
    assertEquals(PasswordStrength.STRONG, result);
}

 

e.g. 4. 실행 결과로 익셉션 발생하는 경우

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

 

외부 상황 & 외부 결과

  • 외부 요인에 의한 상황을 테스트해야 할 때
  • 테스트는 실행 때마다 동일한 결과를 보장해야 하는데, 우연에 의해 테스트 결과가 달라지면 동일한 결과 보장 x

 

e.g. 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("fail givenNoFile: " + path);
        }
    }
}

 

e.g. 2. 파일이 존재하는 상황 : 상황에 알맞은 파일을 미리 만들기

@Test
void dataFileSumTest() {
    File dataFile = new File("src/test/resources/datafile.txt");
    long sum = MathUtils.sum(dataFile);
    assertEquals(10L, sum);
}
  • 다른 개발자도 테스트를 실행할 수 있어야 하기에, 테스트에 맞게 준비한 파일은 버전 관리 대상에 추가해야 함

 

e.g. 3. 파일이 존재하는 상황 : 파일을 미리 만들지 않고, 테스트 코드에서 상황에 맞는 파일 생성하기

@Test
void dataFileSumTest2() {
    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);
    }
}
  • 테스트 코드 안에 필요한 것이 다 있음
  • 테스트 코드에서 상황을 명시적으로 구성하기에, 테스트 내용을 이해하기 위해 많은 파일을 볼 필요 없음

 

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

  • 외부 상태에 따라 테스트 성공 여부가 바뀌지 않으려면, 테스트 실행 전에 외부를 원하는 상태로 만들거나 테스트 실행 후 외부 상태를 원래대로 되돌려놔야 함
  • e.g. DB 데이터의 상태에 따라 테스트 결과가 달라지는 경우 : 중복 발생 방지 or 메서드 실행 후 트랜잭션을 롤백 등

 

외부 상태와 테스트 어려움

  • 외부 요인 예시 : 파일, DBMS, 외부 서버 등에서 발생
    • e.g. 1.  API 응답 시간이 느릴 때
    • e.g. 2. 실행 결과가 외부 시스템에 기록되는데, 데이터에 대해 INSERT, SELECT 권한만 주어질 때
  • ➡️ 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인 '대역'을 사용하면 외부 상황이나 결과를 대체할 수 있어 테스트 작성이 쉬워짐

댓글