6 minute read

TDD에 대한 간단한 정리를 하려한다. 이 포스트에선 실습보단 개념 위주로 적어보려한다.

0. 목차

  1. TDD란?
  2. 테스트 코드 작성 목적
  3. JUnit이란?
  4. JUnit 모듈
  5. JUnit LifeCycle Annotation
  6. JUnit Annotation
  7. 메타 어토테이션 & 컴포즈 어노테이션
  8. 테스트 클래스와 메소드
  9. 디스플레이 네임

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); // 통과
      }
      }
      
  • @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
  1. BeforeAll
  2. BeforeEach
  3. AfterEach
  4. 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 활용

화면 캡처 2022-05-14 100118

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/

Categories:

Updated: