[Java JUnit5] (1) 테스트 코드 적용하기 && Annotaion
TDD에 대한 간단한 정리를 하려한다. 이 포스트에선 실습보단 개념 위주로 적어보려한다.
0. 목차
- TDD란?
- 테스트 코드 작성 목적
- JUnit이란?
- JUnit 모듈
- JUnit LifeCycle Annotation
- JUnit Annotation
- 메타 어토테이션 & 컴포즈 어노테이션
- 테스트 클래스와 메소드
- 디스플레이 네임
1. TDD란?
TDD는 테스트 주도 개발이라는 의미를 가진다. 단순하게 표현하자면 테스트를 먼저 설계 및 구축 후 테스트를 통과할 수 있는 코드를 짜는 방향 코드 작성 후 테스트를 진행하는 지금까지 사용된 일반적인 방식과는 차이가 있다.
애자일 개발 방식
- 코드 설계시 원하는 단계적 목표에 대해 설정하여 진행하고자 하는 것에 대한 결정 방향의 갭을 줄이고자 함
- 최초 목표에 맞춘 테스트를 구축하여 그에 맞게 코드를 설계하기 떄문에 보다 적은 의견 충돌을 기대할 수 있다. (방향 일치로 인한 피드백과 진행 방향의 충돌 방지)
2. 테스트 코드 작성 목적
코드의 안정성을 높일 수 있다. 기능을 추가하거나 변경하는 과정에서 발생할 수 있는 Side-Effect를 줄인다. 해당 코드가 작성된 목적을 명확하게 표현할 수 있다.
- 코드에 불필요한 내용이 들어가는 것을 줄인다.
3. JUnit이란?
Java 진영의 대표적인 Test Framework
- 단위 테스트 (Unit Test)를 위한 도구를 제공한다. ``` 단위 테스트
- 코드의 특정 모듈이 의도된 대로 동작하는지에 대한 테스트를 하는 절차를 의미한다.
- 모든 함수와 메소드에 대한 각각의 테스트 케이스를 작성한다. ```
- 어노테이션을 기반으로 테스트를 지원한다.
- Assert(단정문) 으로 테스트 케이스의 기대값에 대해 수행 결과를 확인할 수 있다.
- Spring Boot 2.2 버전부터 JUnit5 버전을 사용한다.
- JUnit5는 크게 Jupiter, Platform, Vintage 모듈로 구성된다.
- JUnit5는 java8 부터 지원하며, 이전 버전으로 작성된 테스트 코드여도 컴파일이 정상적으로 지원된다.
4. JUnit5 모듈
이전 Junit 버전들과는 다르게 Junit5는 세개의 서브 프로젝트로 이루어져있다. Junit5 는 JUnit Platform + Junit Jupiter + JUnit Vintage 이 세개가 합쳐진 것이다.
4-1. JUnit Platform
Test를 실행하기 위한 뼈대 Test를 발견하고 테스트 계획을 생성하는 TestEngine __인터페이스__를 가지고 있다. TestEngine을 통해 Test를 발견하고, 수행 및 결과를 보고한다. 그리고 각종 IDE 연동을 보조하는 역할을 수행한다. (콘솔 출력)
즉 JVM에서 테스트 프레임워크를 실행하는데 기초를 제공한다고 보면된다. 또한 TestEngine API를 제공해 테스트 프레임워크를 개발할 수 있다.
4-2. JUnit Jupiter
TestEngine API 구현체로 JUnit 5를 구현하고 있다. 테스트의 실제 구현체는 별도 모듈 역할을 수행하는데, 그 모듈 중 하나가 Jupiter-Engine이다. 이 모듈은 Jupiter-AP를 사용하여 작성한 테스트 코드를 발견하고 실행하는 역할을 수행한다. 개발자가 테스트 코드를 작성할때 사용된다.
4-3. JUnit Vintage
JUnit Vintage는 하위 호환성을 위해 JUnit3와 JUnit4를 기반으로 돌아가는 플래솦ㅁ에 테스트 엔진을 제공한다.
5. JUnit LifeCycle Annotation
- @Test: 테스트용 메소드를 표현하는 어노테이션
아래는 JUnit Jupiter에서 테스트를 작성하기 위한 최소 조건으로 테스트를 작성한 예이다.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DemoApplicationTests {
private final Hello hello = new Hello();
@Test
void contextLoads() {
assertEquals("Hello", hello.sayHello());
}
}
- @BeforeEach: 각 테스트 메소드가 시작 되기 전에 실행되어야 하는 메소드 표현
- @Test, @RepeatTest, @ParameterizeTest, @TestFactory가 붙은 테스트 메소드가 실행하기 전에 실행된다. JUnit4의 @Before 와 같은 역할을 한다. 개인적으로 테스트 하기 전에 필요한 목업 데이터를 미리 세팅해 주기 위해 사용한다.
class DemoApplicationTests { private static List<Integer> list; private static int sum; Calculator calculator = new Calculator(); @BeforeEach void init() { sum = 0; list = Arrays.asList(1, 2, 3, 4, 5); } @Test void sum() { sum += calculator.add(list); assertEquals(15, sum); // 통과 } @Test void sum2() { sum += calculator.add(list); assertEquals(15, sum); // 통과 } }
- @Test, @RepeatTest, @ParameterizeTest, @TestFactory가 붙은 테스트 메소드가 실행하기 전에 실행된다. JUnit4의 @Before 와 같은 역할을 한다. 개인적으로 테스트 하기 전에 필요한 목업 데이터를 미리 세팅해 주기 위해 사용한다.
- @BeforeAll: 테스트 시작 전에 실행되어야 하는 메소드 표현(static 처리 필요)
- @BeforeEach는 각 테스트 메소드 마다 실행되지만, 이 어노테이션은 테스트 시작되기 전 딱 한번 만 실행한다.
-
@BeforeAll은 static 혹은 @TestInstance(Lifecycle.PER_CLASS) 붙여 사용해야 한다 ```java class DemoApplicationTests {
private static List
list; private static int sum; Calculator calculator = new Calculator(); @BeforeEach void init() { sum = 0; list = Arrays.asList(1, 2, 3, 4, 5); } @Test void sum() { sum += calculator.add(list); assertEquals(15, sum); // 성공 } @Test void sum2() { sum += calculator.add(list); assertEquals(15, sum); // 실패 }
}
- @AfterEach: 각 테스트 메소드가 시작된 후 실행되어야 하는 메소드를 표현
- @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 가 붙은 테스트 메소드가 실행되고 난 후 실행된다. JUnit4의 @After 어노테이션과 같은 역할
- @AfterAll: 테스트 종료후에 실행되어야 하는 메소드 표현(static 처리 필요)
#### 5-2 JUnit Life Cycle 순서
```java
class DemoApplicationTests {
@BeforeAll
static void beforeAll() {
System.out.println("beforeAll");
}
@BeforeEach
void beforeEach() { System.out.println("beforeEach"); }
@AfterAll
static void afterAll() { System.out.println("afterAll"); }
@AfterEach
void afterEach() { System.out.println("afterEach"); }
@Test
void test1() { System.out.println("test1"); }
@Test
void test2() { System.out.println("test2"); }
}
결과
beforeAll
beforeEach
test1
afterEach
beforeEach
test2
afterEach
afterAll
- BeforeAll
- BeforeEach
- AfterEach
- AfterAll 순으로 실행이 된다.
6. JUnit Annotation
@SpringBootTest
- 통합 테스트 용도로 사용된다.
- @SpringbootApplication을 찾아가 하위의 모든 Bean을 스캔하여 로드한다.
- 그후 Test용 Application Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체한다.
@Nested
- 테스트 클래스안에 Nested 테스트 클래스를 작성할 때 사용된다. static이 아닌 중첩 클래스, 즉 Inner 클래스여야 한다. 테스트 인스턴스 라이프 사이클이 per-class로 설정되어 있지 않다면 @BeforeAll, @AfterAll 가 동작을 안한다.
@ExtendWith
- JUnit4 에서 @RunWith로 사용되던 어노테이션이 ExtendWith로 변경되었다.
- @ExtendWith는 메인으로 실행될 Class를 지정한다.
- 이 어노테이션은 상속이 된다. 확장팩같은 개념
- @SpringBootTest는 기본적으로 @ExtendWith가 추가되어 있다.
@RegisterExtension
- 필드를 통해 extension을 등록한다. 이런 필드는 private이 아니라면 상속이 된다.
@TempDir
- 필드 주입이나 파라미터 주입을 통해 임시적인 디렉토리를 제공할 때 사용한다.
@Tag
- 테스트를 필터링할 떄 사용한다. 클래스 또는 메소드 레벨에 사용한다.
@Disabled
- 테스트 클래스나, 메소드의 테스트를 비활성화 한다.
- JUnit4 의 @Ignore와 같다.
@WebMvcTest(ClassName.class)
- ()안에 작성된 클래스만 실제로 로드하여 테스트를 진행한다.
- 매개변수를 지성해 주지 않으면 @Controller, @RestController, @RestControllerActive 등 컨트롤러와 연관된 Bean이 모두 로드된다.
- 스프링의 모든 Bean을 로드하는 SpringBootTest 대신 컨트롤러 관련 코드만 테스트할 경우 사용한다.
@Autowired about Mockbean
- Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입 받음
- Perform()메소드를 활용하여 컨트롤러의 동작을 확인할수 있따.
- andExpect(), andDo(), andReturn() 등의 메소드를 같이 활용함,
@MockBean
- 테스트할 클래스에서 주입 받고 있는 객체에 대해 가짜 객체를 생성해주는 어노테이션
- 해당 객체는 실제 행위를 하지 않음
- given() 메소드를 활용하여 가짜 객체의 동작에 대해 정의하여 사용한다.
@AutoConfigureMockMvc
- spring.test.mockmvc의 설정을 로드하면 MockMvc의 의존성을 자동으로 주입한다.
- MockMvc 클래스는 Rest API 테스트를 할 수 있는 클래스
@Import
- 필요한 Class들을 Configuration으로 만들어 사용한다.
- Configuration Component 클래스도 의존성 설정을 할 수 있다
- Import된 클래스는 주입으로 사용 가능하다.
7. 메타 어토테이션 & 컴포즈 어노테이션
JUnit Jupiter 어노테이션은 메타 어노테이션 처럼 사용이 된다. 자동으로 메타 어노테이션을 상속하는 자기만의 컴포즈 어노테이션을 적용할수 있다는것
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast { }
@Fast
@Test
void myFastTest() {}
8. 테스트 클래스와 메소드
8-1 테스트 클래스
- 최상위 클래스, static 멤버 클래스, @Nested 클래스에 적어도 한개의 @Test 어노테이션이 달린 테스트 메소드가 포함되어 있는걸 말한다.
- 테스트 클래스는 abstract 이면 안되고, 하나의 생성자가 있어야 한다.
- 생성자가 없으면 컴파일러가 자동으로 생성자를 만들어 준다.
8-2 테스트 메소드
- @Test, @RepeatedTest, @ParamterizedTest, @TestFactory, @TestTemplate 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.
8-3 라이프 사이클 메소드
- @BeforeAll , @AfterAll , @BeforeEach , @AfterEach 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.
- 테스트 메소드와 라이프사이클 메소드는 테스트 할 클래스, 상속한 부모 클래스 또는 인터페이스에 선언이된다.
- 추가로 테스트 메소드와 라이프사이클 메소드는 abstract 선언하면 안되고, 어떠한 값도 리턴해서는 안된다.
- 테스트 클래스, 테스트 메소드, 라이프사이클 메소드를 꼭 public 으로 선언할 필요는 없지만, private 으로 해서는 안된다.
- 무조건 안되는건 아니다.
@BeforeAll private static void testAll() { System.out.println("testAll"); } @BeforeEach private static void testEach() { System.out.println("testEach"); }
- 위 예제 코드가 무조건 작동을 안하는건 아니다. JUnit에서는 접근 지정자가 private 되지 않기를 권고 하지만, private을 지정해도 잘 돌아간다. 이유는 리플렉션을 사용하여 모두 접근이 가능하게 변경하기 때문
- 무조건 안되는건 아니다.
9. 디스플레이 네임
테스트 클래스와 테스트 메소드는 @DisplayName을 이용해서 테스트 이름을 개발자가 보기 좋게 변경해줄수 있다. 공백이나 특수문자나, 이모지 또한 가능하다.
9-1 Display Name Generators
- JUnit Jupiter은 @DisplayNameGeneration 어노테이션을 통해 테스트 이름을 어떻게 보여줄지 결정할 수 있다.
- Standard: 메소드 이름과 그 뒤에 붙는 괄호를 그대로 보여줌
- Simple: 메소드 이름만 보여줌
- ReplaceUnderscores: 언더스코어를 제거한다.
- IndicativeSentence: 테스트 클래스 이름과 테스트 메소드 이름 괄호를 보여준다
9-2 디스플레이 네임 적용하기
1. properties 활용
junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
@TestPropertySource(locations = "classpath:junit-platform.properties")
class DemoApplicationTests {
@Test
void name_test() {
System.out.println("메소드 이름 = name test");
}
}
2. @DisplayName 어노테이션 안에 값
@Test
@DisplayName("메소드 이름이 이걸로 나와")
void name_test() {
System.out.println("메소드 이름 = 메소드 이름이 이걸로 나와");
}
3. DisplayNameGeneration 어노테이션 안에 있는 DisplayNameGenerator 값
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class 테스트클래스 {
}
10. Assertions
JUnit Jupiter는 JUnit4로 부터 온 assertion 메소드와 새롭게 자바 8 람다식으로 추가된 메소드들이 있다. 모든 JUnit Jupiter assertion은 정적 메소드이며, org.junit.jupiter.api.Assertions 클래스 안에 있다.
참고
https://www.youtube.com/watch?v=SFVWo0Z5Ppo&list=PLlTylS8uB2fBOi6uzvMpojFrNe7sRmlzU&index=21 https://thalals.tistory.com/273 https://donghyeon.dev/junit/2021/04/11/JUnit5-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C/