1. JUnit
가. JUnit 5 시작
•
java 클래스에 대한 테스트 클래스 생성
→ ctrl + shift + t
•
특정 테스트 메소드 실행 단축키
→ ctrl + shift + f10(fn 포함)
•
자동 줄맞춤
→ ctrl + alt + l
나. 기본 주석(basic annotation)
•
@BeforeAll: 본 테스트 시작 전, 한 번만 실행하는 메소드
→ static void type만 가능
@BeforeAll
static void beforeAll(){
System.out.println("before all");
}
Java
복사
•
@AfterAll: 테스트 후, 마지막에 한 번만 실행하는 메소드
→ 마찬가지로 static void type만 사용
•
@BeforeEach: 각각의 테스트 시작 전, 실행하는 메소드
→ static일 필요 없음
@BeforeEach
void beforeEach(){
System.out.println("BeforeEach");
}
Java
복사
•
@AfterEach: 각각의 테스트 실행 후, 실행하는 메소드
•
@Disabled: 해당 테스트 실행 X, 깨진 테스트에 대해 처리, 정리되지 않은 테스트의 경우 등에 사용
@Test
@Disabled
void createSecond() {
Study study = new Study();
assertNotNull(study);
System.out.println("createSecond");
}
Java
복사
다. 테스트 이름 표기
•
TestCase의 이름을 Given, When, Then의 구조로 가지면 테스트 가독성이 좋음
•
각 테스트 케이스에서 실 비교 대상에 대해 expected, actual 이름을 붙여서 가독성을 높일 것
@Test
public void givenRadius_whenCalculateArea_thenReturnArea() {
double actualArea = Circle.calculateArea(2d);
double expectedArea = 3.141592653589793 * 2 * 2;
Assert.assertEquals(expectedArea, actualArea);
}
Java
복사
code from https://www.baeldung.com/java-unit-testing-best-practices
•
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
→ 해당 주석이 붙은 클래스 내 모든 메소드에 대해 실행창 메소드 표기 방식에 변화
→ underscore → blank
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {}
Java
복사
•
@DisplayName(""); : 큰 따옴표 안의 이름으로 메소드 이름 표기
@Test
@DisplayName("Origin")
void create() {
Study study = new Study();
assertNotNull(study);
System.out.println("create");
}
Java
복사
라. Assertion
•
assertEqulas(expected, actual, option: message): 기대되는 값과 실제 값을 비교함. 에러 발생 시 message의 내용이 출력됨
→ message의 내용이 길다면 람다를 사용하여 에러 발생할 경우에만 message를 계산하여 출력하도록 함
void create() {
Study study = new Study();
assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디 생성 시 status는 DRAFT가 되어야 함");
}
Java
복사
// 람다 사용
void create() {
Study study = new Study();
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디 생성 시 status는 DRAFT가 되어야 함");
}
Java
복사
•
assertTrue(boolean condition, message): condition의 결과가 false일 경우, message가 출력됨
void create() {
Study study = new Study(10);
assertTrue(study.getLimit() > 0, "스터디 최대 인원은 1명 이상이어야 함");
}
Java
복사
•
assertAll(executables...): 람다식으로 여러개의 assert문을 추가하여 각각의 assert문의 이상유무를 확인할 수 있음
→ assertAll을 사용하지 않고 순차적으로 assert문을 사용할 경우, 만약 맨 위의 assert문에 에러가 발생하면 이하 모든 assert문의 이상유무 메세지를 확인할 수 없음
void create() {
Study study = new Study(10);
assertAll(
() -> assertNotNull(study),
() -> assertTrue(study.getLimit() > 0, "스터디 최대 인원은 1명 이상이어야 함"),
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디 생성 시 status는 DRAFT가 되어야 함")
);
}
Java
복사
•
assertThrows(executable, expectedType): 예외 발생 확인
•
assertTimeout(duration, executable): 특정 시간 안에 실행이 완료되는지 확인
•
assertNotNull(actual): 값이 null이 아닌지 확인
마. 조건에 따라 테스트 실행하기
•
환경변수의 조건에 따라 특정 테스트 실행
→ OnOS, OnJre, IfSystemProperty, IfEnvironmentVariable 등
@Test
@EnabledOnOs({OS.MAC, OS.LINUX})
void create() {
Study study = new Study(10);
System.out.println("create");
assertNotNull(study);
assertTrue(study.getLimit() > 0, "스터디 최대 인원은 1명 이상이어야 함");
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디 생성 시 status는 DRAFT가 되어야 함");
}
Java
복사
•
assumeTrue(조건), assumingThat(조건, 테스트)
바. 태깅과 필터링
•
Tag: 여러 테스트 케이스 그룹화
→ @Tag로 지정한 테스트케이스에 대해 하나의 그룹으로 묶임
•
Filter: 특정 Tag의 테스트 케이스만 실행
→ Edit Configuration에서 특정 태그에 따라 테스트케이스 실행할 수 있도록 설정
@Test
@DisplayName("스터디 만들기")
@EnabledOnOs({OS.MAC, OS.LINUX, OS.WINDOWS})
@Tag("fast")
void create() {
Study study = new Study(10);
System.out.println("create");
assertNotNull(study);
assertTrue(study.getLimit() > 0, "스터디 최대 인원은 1명 이상이어야 함");
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디 생성 시 status는 DRAFT가 되어야 함");
}
Java
복사
사. Custom Annotation 만들기
•
여러 주석(Annotation)을 포함한 하나의 주석으로 만들 수 있음
// FastTest.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("fast")
@EnabledOnOs({OS.WINDOWS})
public @interface FastTest {
}
// StudyTest.java
@FastTest
@DisplayName("스터디 만들기 빠른버전")
void create() {
Study study = new Study(10);
System.out.println("create");
assertNotNull(study);
assertTrue(study.getLimit() > 0, "스터디 최대 인원은 1명 이상이어야 함");
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디 생성 시 status는 DRAFT가 되어야 함");
}
Java
복사
아. 테스트 반복하기
•
RepeatedTest(int value, string name): value값만큼 특정 테스트를 반복실행
@RepeatedTest(value = 10, name = "repetition {currentRepetition} of {totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo){
System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" + repetitionInfo.getTotalRepetitions());
}
Java
복사
•
ParameterizedTest(String name): 전달한 매개변수를 토대로 테스트 반복 실행
→ 매개변수의 소스로 다양한 annotation 사용 가능
→ @NullSource, @EmptySource, @NullAndEmptySource 등
@ValueSource(strings = {"날씨가", "많이", "추워졌다"})
@ParameterizedTest(name = "{index} message={0}")
void parameterizedTest(String message){
System.out.println(message);
}
Java
복사
•
custom Object type에 대해 하나의 매개변수로 전달할 때, ArgumentConvert 해야 함
→ 다음의 순서로 converting 됨
1.
int(10) → Study(10)
2.
Study(10) → String(10)
3.
String(10)→ Integer(10)
@ValueSource(ints = {10, 20, 30})
@ParameterizedTest(name = "{index} message={0}")
void parameterizedTest(@ConvertWith(StudyConverter.class) Study study){
System.out.println(study.getLimit());
}
static class StudyConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
assertEquals(Study.class, targetType, "can only convert to Study");
return new Study(Integer.parseInt(source.toString()));
}
}
Java
복사
•
두 개 이상의 매개변수를 전달할 때, ArgumentsAccessor를 사용함
→ 매개변수의 sorce는 @CsvSource 사용
→ 하나의 문자열 내 공백을 추가할 때, 작은 따옴표로 해당 문자열을 감싼다
//StudyTest.java
@ParameterizedTest(name = "{index} message={1}")
@CsvSource({"10, '자바 스터디'", "20, '자바 스프링'"})
void parameterizedTest(ArgumentsAccessor argumentsAccessor) {
Study study = new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
System.out.println(study);
}
//Study.java
public Study(int limit, String name) {
this.limit = limit;
this.name = name;
System.out.println(getLimit() + " " + getName() + " " + getStatus());
}
Java
복사
•
Aggregator 사용하여 두 개 이상의 매개변수 전달 가능
@ParameterizedTest(name = "{index} message={1}")
@DisplayName("스터디 만들기")
@CsvSource({"10, '자바 스터디'", "20, '자바 스프링'"})
void parameterizedTest(@AggregateWith(StudyAggregator.class) Study study) {
}
static class StudyAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
return new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
}
}
Java
복사
자. 테스트 인스턴스
•
하나의 테스트 클래스 안의 여러 테스트 메소드는 각각의 테스트 인스턴스를 갖는다
→ 각각의 테스트 메소드는 반드시 서로 독립적으로 작동하도록 작성해야 함
•
JUnit 5에서는 테스트 인스턴스 생성과 관련된 정책을 바꿀 수 있는 옵션이 있음
→ @TestInstance(Lifecycle.PER_CLASS): 테스트 클래스 하나에 테스트 인스턴스는 하나만 생성함
차. 테스트 순서
•
단위 테스트에서 각각의 테스트는 반드시 독립적으로 작동해야 함.
→ 다른 테스트에 대한 의존성이 없어야 함
•
불의존 원칙에 따라 모든 테스트에는 순서를 신경쓰지 않아야 함.
→ 비록 JUnit 5에서는 테스트가 순차적으로 실행되는 것으로 보이나, 이것은 JUnit의 내부 로직에 따라 앞으로 언제든 바뀔 수 있음
•
시나리오 테스트의 경우(Stateful Test), @TestInstance과 함께 @TestMethodOrder, @Order(우선순위)를 적용하여 작성 함
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StudyTest {
int testNum = 1;
@FastTest
@Order(4)
void create() {
Study study = new Study(++testNum);
System.out.println("create " + study.getLimit());
}
Java
복사
2. Mocking Test
가. Mocking이 필요한 이유는?
•
개발자가 관여할 수 없는 대상을 테스트할 때, 대상의 행동을 예측하여 생성한 mocking 객체가 필요
•
개발자가 관여할 수 없는 객체의 예로 외부 API 호출이 대표적인데, 외부 API 호출에 따른 예상 응답 등을 mocking하여 객체를 만들어야 외부 API 호출에 따른 연계 기능의 정상 동작여부를 테스트할 수 있다
•
MVC 패턴의 Controller를 테스트하는 경우에도 Response Header에 대한 테스트를 할 때, Mocking 객체를 사용한다. 대다수의 Framework에서 Controller 객체에 정의된 주요 메소드를 호출할 때, ResponseBody 내 주요 데이터만 반환하도록 추상화되었기 때문에 그 외의 값(ex HTTP Status Code 등)을 테스트할 때 Mocking된 객체를 사용하면 효율적이다.
나. Mocking Test도 단위테스트인가?
•
단위테스트에 대한 정의는?
◦
classic style: 반드시 객체를 고립시킬 필요는 없다. 객체의 행동을 하나 단위로써 묶을 수 있다면 그 하나의 행동에 대한 테스트가 단위테스트가 될 수 있다. 예를 들어, Todo List와 같은 단순 CRUD 기능의 HTTP API에서 모든 Todo를 불러오는 GET 요청에 대한 요청 처리와 응답에 대한 테스트를 하나의 단위테스트로 볼 수 있다. 단 외부 객체와 같이 통제할 수 없는 객체는 일부 classic style에서도 mocking하는 것을 권한다.
◦
mockist style: 모든 객체를 고립시킨 후 모든 객체 각각의 메소드에 대한 테스트가 단위테스트다. 개발자가 고립시킬 수 없는 객체(외부 API 등)는 물론이고 개발자가 통제할 수 있는 모든 객체 마저도 고립시켜서 테스트를 작성해야 한다. 예를 들어, Todo List와 같은 단순 CRUD 기능의 HTTP API에서 모든 Todo를 불러오는 GET 요청에 대한 요청 처리와 응답에 대한 단위테스트를 작성한다면, Controller, Service, Model 객체 각각을 고립시켜서 단위테스트를 작성해야 한다
다. Mock 객체를 생성하는 방법은?
•
어노테이션 생성:
◦
생성자 주입: 주로 사용하는 방법으로 테스트 클래스에 전체적으로 사용할 Mock 객체 주입 시 사용
◦
필드 주입: 테스트 메소드별로 주입할 때 사용
•
Mockito.mock() 메소드 생성:
라. Mock 객체의 행동을 정의하는 방법은?
•
mock 객체는 기본적인 행동패턴이 존재함
◦
Null을 리턴한다. (Optional 타입은 Optional.empty 리턴)
◦
Primitive 타입은 기본 Primitive 값.
◦
콜렉션은 비어있는 콜렉션.
◦
Void 메소드는 예외를 던지지 않고 아무런 일도 발생하지 않는다.
•
사용자 정의에 따라 기본적인 행동패턴을 변경할 수 있음
◦
기본 행동정의
◦
◦
BDD 패턴에 따라는 API를 활용하여 다음과 같이 Mock 객체의 행동을 정의할 수 있다
given(memberService.findById(1L)).willReturn(Optional.of(member));
given(studyRepository.save(study)).willReturn(study);
Java
복사
3. 도커와 테스트
4. 성능 테스트
5. 운영 이슈 테스트
6. 아키텍처 테스트
7. 정리
Reference
•
Unit Test Best Practice, https://www.baeldung.com/java-unit-testing-best-practices