* 책 내용의 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);
...
'Study > 테스트 주도 개발' 카테고리의 다른 글
TDD, Mock, SOLID 얘기 - 도시 가스 요금 계산 (0) | 2023.01.29 |
---|---|
[TDD] 스터디 4주차 (25~28장 정리) (0) | 2023.01.20 |
[TDD] 스터디 2주차 (JUnit 관련 내용) (0) | 2023.01.09 |
[TDD] 스터디 1주차 (기본적인 테스트 방법, 1~2장 정리) (0) | 2022.12.18 |
댓글