본문 바로가기
Study/테스트 주도 개발

[TDD] 스터디 3주차 (테스트 반복, 순서 지정, Extension 관련)

by Nahwasa 2023. 1. 10.

스터디 메인 페이지

 

* 책 내용의 1,2부는 책을 따라 실습하는 내용이므로 정리하는게 의미 없다고 판단되어 다른 스터디와 달리 책 내용은 정리하지 않습니다. 스터디에서 책 이외로 나온 내용만 정리했습니다.

 

 

⚈ 예를들어 시간 조건을 두고 테스트가 필요한 경우, 아래와 같은 테스트는 실행할때마다 성공할수도 있고 실패할수도 있다.

@Test
@DisplayName("1000만까지의 짝수의 합은 1ms 이내에 통과해야 한다.")
void test() {
    Study study = new Study();
    long sum = assertTimeoutPreemptively(
            Duration.ofMillis(1)
            ,() -> study.sumEvenNumbers(10000000)
            ,() -> "1000만 이하의 짝수의 합은 10ms 이내로 획득 가능해야 한다."
    );

    assertEquals(25000005000000l, sum, () -> "1000만 까지의 합은 25000005000000 이어야 한다.");
}

 

테스트 반복(RepeatedTest), 인자 넣어서 테스트(ParameterizedTest) (github)

  • 이런 경우 아래와 같이 RepeatedTest로 반복해서 테스트할 수 있다.
@DisplayName("3주차 스터디")
public class StudyTest {
    @RepeatedTest(10)
    @DisplayName("10번 모두 1ms 이내에 통과해야 한다.")
    void repeated_test() {
        Study study = new Study();
        long sum = assertTimeoutPreemptively(
                Duration.ofMillis(1)
                ,() -> study.sumEvenNumbers(10000000)
                ,() -> "1000만 이하의 짝수의 합은 10ms 이내로 획득 가능해야 한다."
        );

        assertEquals(25000005000000l, sum, () -> "1000만 까지의 합은 25000005000000 이어야 한다.");
    }
}

----

public class Study {

    public long sumEvenNumbers(int num) {
        long sum = 0l;
        for (int i = 0; i <= num; i++) {
            if (i%2 == 0)
                sum += i;
        }
        return sum;
    }
}

 

  • 또한 ms 등을 변경해서 하려면 ParameterizedTest를 쓰면 좋다.
@DisplayName("지정된 ms 이내 가능한지 테스트")
@ParameterizedTest(name = "{index}번째: {displayName} ms = {0}")
@ValueSource(ints = {10, 5, 3})
void parameterized_test(int ms) {
    Study study = new Study();
    assertTimeoutPreemptively(
            Duration.ofMillis(ms),
            () -> study.sumEvenNumbersUnderN(10000000),
            () -> "1부터 1억까지의 짝수의 합은 "+ ms +"ms 이내로 획득 가능해야 한다."
    );
}

@DisplayName("지정된 횟수가 지정된 ms 이내 가능한지 테스트")
@ParameterizedTest(name = "{index}번째: {displayName} ms = {0}, cnt = {1}")
@CsvSource({"1, 1000", "2, 10000", "5, 100000"})
void parameterized_cvs_test(int ms, int num) {
    Study study = new Study();
    assertTimeoutPreemptively(
            Duration.ofMillis(ms),
            () -> study.sumEvenNumbersUnderN(num),
            () -> "1부터 "+ num +"까지의 짝수의 합은 "+ ms +"ms 이내로 획득 가능해야 한다."
    );
}

@DisplayName("지정된 횟수가 지정된 ms 이내 가능한지 클래스 형태로 받아서 테스트")
@ParameterizedTest(name = "{index}번째: {displayName} ms = {0}, cnt = {1}")
@ValueSource(ints = {10, 5, 3})
void parameterized_class_value_source_test(@ConvertWith(TimeLimitConverter.class) TimeLimit timeLimit) {
    System.out.println(timeLimit.ms);
    Study study = new Study();
    assertTimeoutPreemptively(
            Duration.ofMillis(timeLimit.ms),
            () -> study.sumEvenNumbersUnderN(10000000),
            () -> "1부터 10,000,000까지의 짝수의 합은 "+ timeLimit.ms +"ms 이내로 획득 가능해야 한다."
    );
}

static class TimeLimitConverter extends SimpleArgumentConverter {   // 하나의 인자만 사용할 때.
    @Override
    protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
        assertEquals(TimeLimit.class, targetType, "TimeLimit 으로만 변환 가능하다.");
        return new TimeLimit(Integer.parseInt(source.toString()));
    }
}

 

 

테스트 순서 지정. (github)

  • 기본적으론 서로 의존성 없이 독립적으로 수행 가능해야 하므로 테스트 순서는 의미가 없다. 테스트도 작성 순서와 관계 없이 JUnit에서 특정 규칙으로 알아서 지정한 순서대로 수행된다.

 

  • 하지만 통합테스트를 할 때, 의존성을 가지더라도 원하는 순서대로(로그인 후 그걸 가지고 다른 테스트진행과 같이) 실행하고 싶을 수 있다. 이 경우 아래와 같이 순서를 지정 가능하다. Order내의 값은 작을수록 우선순위가 높다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StudyTest {

    @Test
    @Order(3)
    @DisplayName("세 번재 테스트")
    void testA() {
        System.out.println(3);
    }

    @Test
    @Order(2)
    @DisplayName("두 번째 테스트")
    void testB() {
        System.out.println(2);
    }

    @Test
    @Order(1)
    @DisplayName("첫 번재 테스트")
    void testC() {
        System.out.println(1);
    }
}

 

  • 다만 어차피 의존성을 가지고 하려는건데, 기본적으로 테스트는 테스트마다 클래스를 새로 생성하므로 전역변수가 공유되지 않는다. 즉 아래와 같이 num을 공유하고 싶어도 안된다.
@DisplayName("3주차 스터디")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StudyTest {
    int num = 0;

    @Test
    @Order(3)
    @DisplayName("세 번재 테스트")
    void testA() {
        System.out.println(num++);
    }

    @Test
    @Order(2)
    @DisplayName("두 번째 테스트")
    void testB() {
        System.out.println(num++);
    }

    @Test
    @Order(1)
    @DisplayName("첫 번재 테스트")
    void testC() {
        System.out.println(num++);
    }
}

// 출력 : 0, 0, 0

 

  • 이런 경우 아래처럼 라이프사이클을 지정해줄 수 있다. (github)
@DisplayName("3주차 스터디")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class StudyTest {
    int num = 0;

    @Test
    @Order(3)
    @DisplayName("세 번재 테스트")
    void testA() {
        System.out.println(num++);
    }

    @Test
    @Order(2)
    @DisplayName("두 번째 테스트")
    void testB() {
        System.out.println(num++);
    }

    @Test
    @Order(1)
    @DisplayName("첫 번재 테스트")
    void testC() {
        System.out.println(num++);
    }
}

// 출력 : 0, 1, 2

 

  • 또는 test쪽에 resources 폴더를 만들고 junit-platform.properties를 만들어 설정할 수도 있다. (github)

junit.jupiter.testinstance.lifecycle.default=per_class

 

 

Extension (github)

  • @BeforeEach 같은 부분을 JUnit Extension Model을 사용해 만들어줄 수 있다.
  • 예를들어 테스트가 생각보다 느릴 경우, 어떤게 느린 테스트인지 확인하고 싶다. 그럴 경우 아래와 같이 짤 수 있다.
public class WarningSlowExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    private static final long LIMIT = 1000l;
    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        String className = context.getRequiredTestClass().getName();
        String methodName = context.getRequiredTestMethod().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(className, methodName));
        store.put("startAt", System.currentTimeMillis());
    }
    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        String className = context.getRequiredTestClass().getName();
        String methodName = context.getRequiredTestMethod().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(className, methodName));
        long startAt = store.get("startAt", long.class);
        long duration = System.currentTimeMillis() - startAt;
        if (duration > LIMIT)
            System.out.println(methodName + " 테스트가 느립니다.");
    }

}

 

  • 이 때 특정 어노테이션이 걸린건 아래처럼 빼줄 수 있다. 예를들어 위에 테스트 반복쪽에서 빠르게 수행되는지 확인하는 부분은 어차피 일정 시간 돌아야하니 시간을 재고싶지 않다면 아래처럼 해볼 수 있다.
public class WarningSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    private int DURATION_LIMIT_MS;

    public WarningSlowTestExtension() {
        this(10);
    }

    public WarningSlowTestExtension(int DURATION_LIMIT_MS) {
        this.DURATION_LIMIT_MS = DURATION_LIMIT_MS;
    }

    private static ExtensionContext.Store getStore(ExtensionContext context) {
        String className = context.getRequiredTestClass().getName();
        String methodName = context.getRequiredTestMethod().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(className, methodName));
        return store;
    }
    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        ExtensionContext.Store store = getStore(context);   // 클래스명과 메소드명으로 store 획득
        store.put("START_AT", System.currentTimeMillis());  // 시작 시간을 저장
    }

    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        String methodName = context.getRequiredTestMethod().getName();
        RepeatedTest repeatedTestAnnotation = context.getRequiredTestMethod().getAnnotation(RepeatedTest.class);
        ParameterizedTest parameterizedTestAnnotation = context.getRequiredTestMethod().getAnnotation(ParameterizedTest.class);

        ExtensionContext.Store store = getStore(context);
        long startAt = store.remove("START_AT", long.class);    // beforeTestExecution에서 넣은 시작시간을 꺼내옴
        long duration = System.currentTimeMillis() - startAt;   // 얼마나 걸렸는지
        if (duration > DURATION_LIMIT_MS && repeatedTestAnnotation == null && parameterizedTestAnnotation == null)  // 지정한 시간이 넘는데, Reapeated나 Parameterized가 아니라면 메시지 출력
            System.out.println(methodName + " 테스트는 " + DURATION_LIMIT_MS + "ms 초과로 시간이 걸립니다.");
    }
}

 

  • 이렇게 만든 Extension은 @ExtendWith나, @RegisterExtension으로 적용시킬 수 있다. 후자의 경우 생성자에 원하는 값을 넣을 수 있어서 좋다. (이하 코드에서 둘 중 하나를 선택하면 된다.)
@ExtendWith(WarningSlowTestExtension.class) // Extension 적용. 좀 더 확장성있게 하려면 RegisterExtension으로.
class StudyTest {

    @RegisterExtension  // 위 ExtendWith로는 이것처럼 생성자에 값을 넣어 지정해줄 순 없다. 그 차이임.
    static WarningSlowTestExtension warningSlowTestExtension = new WarningSlowTestExtension(1);
    
    ...

댓글