Spring Boot - JUnit을 활용한 TDD

진경천·2024년 11월 26일

테스트 주도 개발(TDD)이란?

Test Driven Development

테스트 코드를 먼저 작성하고 그 테스트를 통과하는 실제 코드를 작성하는 개발 방법론이다.

TDD 원칙

  1. 실패 테스트 작성
  2. 테스트 통과 가능 최소 코드 작성
  3. 리팩토링

TDD 장점

  • 코드 품질 향상
  • 버그 감소
  • 문서화 개선
  • 설계 개선
  • 리펙토링 용이

JUnit

Java를 위한 대표적인 단위 테스팅 프레임워크

  • 단위 테스트를 위한 도구 제공
  • 어노테이션 기반으로 테스트 지원
  • Assert(단정문)으로 테스트 케이스의 기대값에 대한 수행결과를 확인 가능

SpringBoot 2.2 이상의 버전을 사용한다면 JUnit5 의존성이 추가되어있다!

최신버전은 JUnit5이며
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
라고 표현할 수 있다.

JUnit Jupiter: TestEngine API 구현체로, JUnit5를 구현하고 있음

JUnit Platform: Test를 실행하기 위한 뼈대로 테스트 코드 작성에 필요한 junit-jupiter-api 모둘과 테스트 ㅅ길행을 위한 junit-jupiter-engine 모듈로 분리되어 있다.

Junit Vintage: JUnit3, JUnit4를 실행할 수 있는 TestEngine으로 하위버전의 호화능ㄹ 위해서 존재

Test 메서드

@Test
public void test(){
	// 테스트 코드
}
  • @Test: 해당 메서드가 테스트 메서드임을 나타냄
  • @ParametherizedTest: 해당 메서드가 매개변수가 있는 테스트임을 나타냄
  • @RepeatedTest(n) : n만큼 반복되는 테스트 메서드임을 나타냄

어노테이션을 이용해 메서드가 테스트 메서드임을 명시하고, Assertion 예외를 발생시키지 않으면 성공으로 간주한다.

@DisplayName

  • 테스트 목적을 명확히 표현
  • 복잡한 비즈니스 규칙 설명 가능
  • 테스트 결과 보고서의 가독성 향상
  • 비개발자와의 커뮤니케이션 개선
  • 테스트 문서화 효과
@Test
@DisplayName("새로운 게시글 추가 시 글이 저장되고 201 상태코드가 반환된다.")
void createArticleTest() throws Exception {

}

@Test
@DisplayName("존재하지 않는 ID로 게시글 조회시 404 에러가 발생한다.")
void getArticleNotFoundTest(){

}

JUnit 생명주기

JUnit 테스트의 시작부터 종료까지의 과정으로 각 단계에서는 특정 작업이 수행 되며 이를 통해 테스트가 원할하게 진행된다.

  1. 테스트 클래스 인스턴스 새로 생성

  2. 생성한 클래스 인스턴스가 가지고 있는 테스트 환경준비(setup) 메서드를 찾아 모두 호출

    • setup 메서드는 각 테스트가 실행되기전 호출
    • setup 메서드를 통해 테스트에 필요한 초기 설정을 할 수 있다.
  3. 테스트 메서드 호출

  4. 테스트 클래스 인스턴스가 가지고 있는 테스트 환경정리(teardown) 메서드를 찾아 모두 호출

    • teardown 메서드는 각 테스트가 종료된 후에 호출
    • 테스트에서 사용한 리소스를 해제하거나 테스트 데이터를 정리하는 작업을 하여 다음 테스트가 깨끗한 상태에서 시작될 수 있게 해준다.

setup: 초기 환경 설정
teardown: 테스트 환경 정리

LifeCycle Annotation

어노테이션 설명
@BeforeEach 각 테스트 메서드가 실행되기 전 실행되는 메서드를 나타냄
@BeforeAll 테스트 클래스의 시작 전에 한번 수행되는 메서드를 나타냄
@AfterEach 각 테스트 메서드가 실행 후에 실행되는 메서드를 나타냄
@AfterAll 테스트 클래스의 실행 후에 실행되는 메서드를 나타냄
public class LifeCycleTest {
    @Test
    void testMethod1(){
        System.out.println("testMethod1 실행");
        assertEquals(2, 1+ 1);
        System.out.println("testMethod1 assertEquals 완료");
    }

    @Test
    void testMethod2(){
        System.out.println("testMethod2 실행");
        assertEquals(7, 1 + 2);
        System.out.println("testMethod2 assertEquals 완료");
    }

    @BeforeAll
    static void setupBeforeAll(){
        System.out.println("모든 테스트 메서드 실행 전");
    }

    @AfterAll
    static void tearDownAfterAll(){
        System.out.println("모든 테스트 메서드 실행 후");
    }

    @BeforeEach
    void setupBeforeEach(){
        System.out.println("각 테스트 메서드 실행 전");
    }

    @AfterEach
    void tearDownAfterEach(){
        System.out.println("각 테스트 메서드 실행 후");
    }
}

Assert API

  • 테스트 중에 특정 조건이 참인지 확인하는 함수로, 테스트 케이스의 실행 결과가 예상대로 인지 확인함
  • Assert 메서드의 조건이 만족되지 않으면 assert 예외를 던지며 테스트는 실패로 간주
메서드 설명
assertEquals(expected, actual) 기대값(expected)과 실제값(actual)이 동일한지 확인
assertNotEquals(unexpected, actual) 기대하지않은 값과 실제값(actual)이 틀린게 맞는지확인
assertTrue(condition) 주어진 조건(condition) 이 true인지 확인
assertFalse(condition) 주어진 조건(condition)이 false인지 확인
assertNull(object) 주어진 객체가 null인지 확인
assertNotNull(Object) 주어진 객체가 null이 아닌지 확인
assertSame(expected, actual) expected와 actual이 동일한 객체를 참조하는지 확인
assertNotSame(unexpected, actual) unexpected와 actual이 다른 객체를 참조하는지 확인
assertArrayEquals(expectedArray, actualArray) 두 배열이 동일한 순서와 값을 가지는지 확인
assertThrows(expectedException, executable) 주어진 executable이 expectedException을 발생시키는지 확인

assertThat

assertThat(T actual, Matcher<? Super T> matcher)

  • actual: 검증하려는 실제 값
  • matcher: 실제 값에 적용되는 조건을 정의하는 객체

주어진 실제 값과 Matcher 객체를 활용해 예상되는 조건을 비교한다.

Hamcrest 라이브러리를 사용해야하며 테스트 코드의 가독성을 향상시킬 수 있다.

String actualString = "Hello, World";
int actualVal = 5;

assertThat(actualString, is("Hello, World"));
assertThat(actualVal, is(5));
assertThat(actualString, startsWith("Hello"));
assertThat(actualString, endsWith("World"));
assertThat(actualVal, is(not(10)));

Given When Then 패턴

테스트의 구조를 명확하게 나타내기 위해 사용, 각각의 단계는 테스트의 준비, 실행 그리고 단계를 나타냄

  • Given: 테스트의 전제 조건을 설정하는 단계
  • When: 실제로 테스트하려는 로직을 ㅎ실행하는 단계
  • Then: 실행 결과를 검증하는 단계
@Test
public void testAddition(){
    // given 테스트에 필요한 데이터나 상태를 준비
    SimpleCalculator calculator = new SimpleCalculator();
    int a = 2;
    int b = 3;
    // when 실제로 테스트하려는 동작을 수행
    int res = calculator.subtract(a, b);
    // then 결과 검증
    assertEquals(5, res);
}

Mock 객체 활용 테스트

Mock의 사전적 의미는 가짜의, 모조품으로, 실제 객체를 만들기엔 비용가 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로된 구현이 어려울 경우, 가짜 객체를 사용하는데 이것을 Mock이라고 한다.

실제 객체와 동일한 모의 객체를 만들어 테스트의 효용성을 높이기 위해 사용한다.

Mock 객체 활용의 장점

  • 테스트의 단순화 및 격리
  • 의존성 분리
    • 외부 의존성을 모방하여 테스트 환경에서만 동작한다.
  • 특정 상황 및 예외 모방

Mockito

Java를 위한 Mokcing 프레임워크
Mock 객체의 생성 및 관리를 도와준다

테스트의 격리를 유지하며 테스트 대상 코드와 의존성 사이의 상호작용을 검증 또는 특정 상황을 시뮬레이션 할 수 있다.

class BlogServiceTest {

    @Mock
    private BlogRepository blogRepository;

    @InjectMocks
    private BlogService blogService;

    @BeforeEach void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    @DisplayName("블로그 글 업데이트 테스트")
    void updateArticleTest() {
    
        //given
        Long articleId = 1L;
        Article existingArticle = new Article("Old Title", "Old Content");
        UpdateArticleRequest request = new UpdateArticleRequest("New Title", "New Content");
        when(blogRepository.findById(articleId)).thenReturn(Optional.of(existingArticle));
        
        //when
        Article updatedArticle = blogService.update(articleId, request);
        Article test = blogService.findById(articleId);
        
        //then
        assertNotNull(updatedArticle);
        assertEquals(updatedArticle.getTitle(), "New Title");
        assertEquals(updatedArticle.getContent(), "New Content");
        assertEquals(test.getTitle(), "New Title");
        verify(blogRepository, times(2)).findById(articleId);
    }
    
}

@Mock 선언한 객체가 Mock 객체임을 의미
@InjectMocks Mock 객체를 선언한 객체에 주입한다는 것을 의미
when() Mock 객체가 수행할 메서드와 그에 따른 반환값을 지정해준다.

Slice Test

MVC 패턴을 레이어 별로 잘라서, 레이어를 하나의 단위로 보는 단위 테스트를 의미한다.

@ExtendWith(SpringExtension.class)
@WebMvcTest(GreetingController.class)
public class MockMVCTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGreetWithoutName() throws Exception {
        ResultActions res = mockMvc.perform(get("/greeting"));

        res.andExpect(status().isOk())
                .andExpect(content().string("Hello, World!"));
    }
}
  • MockMvc: 클라이언트의 요청을 테스트할 컨트롤러로 전달하는 역할 수행
    • get(), post(), put(), deleate() 등의 메서드로 요청을할 수 있다.
  • ResultActions: andExpect() 메서드를 통해 컨트롤러의 결과를 검증
profile
어중이떠중이

0개의 댓글