처음부터 차근차근

테스트 주도개발(TDD) 본문

Framework/Spring

테스트 주도개발(TDD)

_soyoung 2022. 8. 14. 12:51
반응형

테스트 코드를 작성하는 이유

  1. 개발 과정에서 문제를 미리 발견할 수 있다.
  2. 리팩토링의 리스크가 줄어든다.
  3. 문서로서의 기능을 한다.

테스트 코드를 작성하는 가장 큰 목적이 첫 번째 이유이다.

테스트 코드를 작성하고 테스트하는 것은 코드에 잠재된 문제를 발견하는데 큰 도움이 된다.

일부러 에러가 발생하는 테스트 코드를 만들어서 예외처리가 잘 되는지 확인하거나,

비즈니스 로직에 맞춰 테스트 코드를 작성해서 결괏값이 참(200)으로 잘 나오는 지 확인하는 등의 방법으로 애플리케이션을 테스트한다.

 

애플리케이션을 개발하면 서비스 업데이트와 유지보수를 위해서 계속해서 코드를 수정하고 추가한다.

코드를 수정하는 것은 다른 애플리케이션 코드에 영향을 주는 위험한 작업이다.

그래서 테스트 코드를 작성하면 수시로 전체 애플리케이션의 동작을 테스트하지 않아도 되고, 혹시 모를 에러에 대비해서 안전하게 유지보수 할 수 있다.

 

이건 몰랐는데 테스트 코드가 문서로서의 기능을 한다고 한다.

프로젝트 협업을 하면 다른 사람의 코드를 파악해야될 때가 있는데, 이 때 테스트 코드가 있으면 다른 사람의 코드를 더 이해하기가 쉽다.

테스트 코드를 애플리케이션 코드와 비교해보면 작성자의 의도를 파악할 수 있기 때문에 다른 사람의 코드를 이해하고 합칠 수 있다.

 

단위 테스트

단위테스트는 애플리케이션의 개별 모듈을 독립적으로 테스트 하는 방식이다.

가장 작은 단위의 테스트 방식이다.

메서드 단위로 테스트하는 것이라고 생각하면 편하다.

소단위로 테스트하는 것이다보니까 간단하고 빠르게 실행할 수 있다는 장점을 가진다.

 

통합 테스트

통합테스트는 애플리케이션의 다양한 모듈을 결합해 전체적인 로직이 정상적으로 동작하는지 테스트하는 방식이다.

단위 테스트보다 넓은 범위의 테스트이며,

데이터베이스나 네트워크 같은 외부 요인들을 포함하고 테스트한다는 특징을 가지고 있다.

넓은 범위의 종속성까지 테스트함으로써 단위 테스트 보다 좀 더 유의미할 수 있다는 장점이 있지만 그만큼 테스트 비용이 커진다는 단점이 있다.

 

Given-When-Then 패턴

Given-When-Then 패턴은 테스트 코드를 표현하는 방식이다.

테스트 주도 개발에서 파생된 BDD(행위 주도 개발)를 통해 생겨난 테스트 접근 방식이다.

보통 인수 테스트에서 사용한다.

 

Given

테스트에 필요한 환경 설정 단계.

ex) 테스트에 필요한 변수 정의, Mock 객체를 통한 행동 정의

 

When

테스트의 목적을 보여주는 단계.

실제 테스트 코드가 있는 곳. 테스트를 통해 결괏값을 반환함.

 

Then

테스트 결과를 검증하는 단계.

when 단계에서 나온 결과값을 검증함.

 

JUnit

자바에서 사용되는 대표적인 테스트 프레임워크이다.

단위 테스트와 통합테스트를 위한 도구들을 제공한다.

어노테이션 기반의 테스트 방식을 지원한다는 특징이 있다.

JUnit을 사용하면 몇개의 어노테이션만으로 간단하게 테스트 코드를 만들 수 있다.

assert를 통해 결괏값이 정상적인지 확인할 수 있다.

 

JUnit5의 모듈 구조

JUnit Platform 

JVM에서 테스트를 시작하기 위한 뼈대 역할.

테스트 엔진의 인터페이스를 가지고 있다.

테스트 엔진 : 테스트를 발견하고, 수행하고, 그 결과를 보고하는 것

 

JUnit Jupiter 

테스트 엔진 API의 구현체를 가지고 있다.

Jupiter Engine을 포함하고 있다.

 

JUnit Vintage

JUnit 3, 4 테스트 엔진 API의 구현체를 가지고 있다.

Vintage Engine을 포함하고 있다.

 

JUnit의 생명주기

* 다 같이 사용했을 때 호출되는 순서대로 정리함

@BeforeAll : 테스트를 시작하기 전 호출되는 메서드 정의

@BeforeEach : 각 테스트 메서드가 실행되기 전 호출되는 메서드 정의

@Test : 테스트 코드를 포함한 메서드 정의

@AfterEach : 각 메서드가 종료되면서 호출되는 메서드 정의

@AfterAll : 테스트를 종료하면서 호출되는 메서드 정의

 

테스트 주도 개발(TDD)

TDD = test-driven development

테스트 주도 개발은 소프트웨어 개발 방법론 중의 하나로, 선 테스트 후 개발 방식의 프로그래밍 방법이다.

먼저 테스트 코드를 작성한 후 테스트를 통과하기 위한 코드를 개발한다.

 

슬라이드 테스트 실습

슬라이드 테스트는 단위 테스트와 통합 테스트의 중간 개념으로, 레이어별로 나눠서 테스트를 진행한다.

단위 테스트를 하려면 모든 외부 요인을 차단해야되지만 그렇게 하면 의미가 없기 때문에 슬라이드 테스트를 진행했다.

 

1. spring-boot-starter-test 의존성 없으면 추가

testImplementation 'org.springframework.boot:spring-boot-starter-test'

스프링 부트에서는 spring-boot-starter-test 프로젝트를 지원해서 테스트 환경을 쉽게 설정할 수 있다.

 

2. 테스트 코드 작성

테스트 코드 파일은 보통 test 폴더 아래에다 생성하고, 프로젝트 폴더 구성과 같게 한다.

프로젝트 폴더
테스트 폴더

테스트 코드는 controller, service, repository마다 작성하는 방법이 조금씩 다르다.

 

controller

given ~ mockMvc ~ verify

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(BoardController.class)
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    BoardServiceImpl boardService;

    @BeforeEach
    void beforeEach() {
        System.out.println("테스트 시작");
    }

    @AfterEach
    void afterEach() {
        System.out.println("테스트 끝");
    }

    @Test
    @DisplayName("게시판 상세 페이지 테스트")
    public void detail() throws Exception {
        // given
        given(boardService.getBoard(1)).willReturn(
                BoardVO.builder()
                        .id(1)
                        .title("테스트")
                        .content("테스트 내용")
                        .writeTime("2022-07-28 18:26:39")
                        .viewCount(0)
                        .state(1)
                        .writerId(2)
                        .build()
        );

        int boardId = 1;

        // when, then
        mockMvc.perform(
            get("/board/" + boardId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").exists())
                .andExpect(jsonPath("$.title").exists())
                .andExpect(jsonPath("$.content").exists())
                .andExpect(jsonPath("$.writeTime").exists())
                .andExpect(jsonPath("$.viewCount").exists())
                .andExpect(jsonPath("$.state").exists())
                .andExpect(jsonPath("$.writerId").exists())
                .andDo(print());

        verify(boardService).getBoard(1);
    }
}

@WebMvcTest(대상 클래스.class)

대상 클래스를 로드해 테스트한다는 뜻이다.

만약 대상 클래스를 작성하지 않으면 컨트롤러 관련 빈 객체들(@Controller, @RestController, @ControllerAdvice 등)을 전부 로드한다.

@SpringBootTest의 경우 모든 빈을 로드하기 때문에 @SpringBootTest보다 가볍게 테스트 하기위해 사용한다.

 

@MockBean

실제 빈 객체가 아닌 가짜(Mock) 객체를 생성해서 주입한다. mock = 속이다

그래서 @MockBean이 선언된 객체는 실제 객체가 아니기 때문에 실제 행위를 수행하지 않는다.

그래서 given()을 이용해 동작을 정의해야한다.

 

MockMvc

Controller를 테스트하는 객체.

perform() 메소드를 이용해서 Controller 호출 테스트를 한다.

 

perporm()

perform() 안에는 controller 호출 방식인 get(“호출URI”), post(“호출URI”), put(“호출URI”), delete(“호출URI”)가 들어갈 수 있다. ex) perform(post(“/test”)))

get() or post() 뒤에 controller 호출 시

header값인 .header(),

accept정보를 설정해주는 .accept(),

JSON이나 XML타입을 결정해주는 .contentType(),

post방식일 경우 body값인 .content(),

get 방식인 경우 파라미터인 .param() 등을 호출할 수 있다.

위에 것들을 여러개 연달아서 사용하는 것도 가능하다.

 

andExpect()

값을 검증하는 코드이다. (expect = 예상하다)

<상태 검증>

.andExpect(status.isOk()) : 성공. 200

.andExpect(status.isNotFound()) : 페이지 찾을 수 없음. 404

.andExpect(status.isMethodNotAllowed()) : 메소드 매칭 안됨. 405

.andExpect(status.isInternalServerError()) : 서버 동작에서 발생하는 에러. 500

<view 검증>

andeExpect(view().name("aaa")) : 컨트롤러가 리턴한 뷰 이름이 "aaa"인지 검증한다.

<리다이렉트 검증>

andExpect(redirectedUrl("/aaa")) : 

 

jsonPath

json 값을 쉽게 처리하게 해주는 표현식이다.

jsonPath를 이용하면 각각의 인자 값을 검증할 수 있다.

ex) .andExpect(jsonPath("$.name").exists())

 

andDo()

요청에 대한 처리를 하는 코드이다.

주로 print()를 많이 쓴다.

 

verify()

지정된 메서드가 실행됐는지 검증하는 메서드

 

 

service

객체 생성, Mokito.when() ~ 실제 service 코드 테스트 ~ Assertions.assertEquals(), verify()

public class BoardServiceTest {

    private BoardRepository boardRepository = Mockito.mock(BoardRepository.class);
    private BoardServiceImpl boardService;

    @BeforeEach
    public void setUpTest() {
        boardService = new BoardServiceImpl(boardRepository);
        System.out.println("테스트 시작");
    }

    @AfterEach
    void afterEach() {
        System.out.println("테스트 끝");
    }

    @Test
    public void getBoardTest() {
        //given
        BoardVO givenBoard = BoardVO.builder()
                .id(1)
                .title("테스트")
                .content("테스트 내용")
                .writeTime("2022-07-28 18:26:39")
                .viewCount(0)
                .state(1)
                .writerId(2)
                .build();

        Mockito.when(boardRepository.findById(1))
                .thenReturn(givenBoard);

        // when
        BoardVO boardVO = boardService.getBoard(1);

        // then
        Assertions.assertEquals(boardVO.getId(), givenBoard.getId());
        Assertions.assertEquals(boardVO.getTitle(), givenBoard.getTitle());
        Assertions.assertEquals(boardVO.getContent(), givenBoard.getContent());
        Assertions.assertEquals(boardVO.getWriteTime(), givenBoard.getWriteTime());
        Assertions.assertEquals(boardVO.getViewCount(), givenBoard.getViewCount());
        Assertions.assertEquals(boardVO.getState(), givenBoard.getState());
        Assertions.assertEquals(boardVO.getWriterId(), givenBoard.getWriterId());

        verify(boardRepository).findById(1);
    }
}

Mockito의 mock() 메서드를 이용해 BoardRepository 클래스의 가짜 객체를 주입받았다.

Assertions.assertEquals( , ) : 왼쪽 매개변수의 값과 오른쪽 매개변수의 값이 같은지 확인하는 함수

 

 

repository

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class BoardRepositoryTest {

    @Autowired
    private BoardRepository boardRepository;

    // given
    Board givenBoard = Board.builder()
            .id(1)
            .title("테스트")
            .content("테스트 내용")
            .writeTime("2022-07-28 18:26:39")
            .viewCount(0)
            .state(1)
            .writerId(2)
            .build();

    @Test
    void createTest() {
        // when
        int result = boardRepository.create(givenBoard);

        // then
        Assertions.assertEquals(result, 1);
    }

    @Test
    void updateTest() {
        // when
        int result = boardRepository.update(givenBoard);

        // then
        Assertions.assertEquals(result, 1);
    }

    @Test
    void findByIdTest() {
        // when
        BoardVO board = boardRepository.findById(givenBoard.getId());

        // then
        Assertions.assertEquals(givenBoard.getId(), board.getId());
        Assertions.assertEquals(givenBoard.getTitle(), board.getTitle());
        Assertions.assertEquals(givenBoard.getContent(), board.getContent());
        Assertions.assertEquals(givenBoard.getWriteTime(), board.getWriteTime());
        Assertions.assertEquals(givenBoard.getViewCount(), board.getViewCount());
        Assertions.assertEquals(givenBoard.getState(), board.getState());
        Assertions.assertEquals(givenBoard.getWriterId(), board.getWriterId());
    }

    @Test
    void deleteTest() {
        // when
        int result = boardRepository.delete(givenBoard.getId());

        // then
        Assertions.assertEquals(result, 1);
    }
}

 

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

끝의 속성값이 NONE이면 임베디드 데이터베이스 즉, 현재 연결되어있는 데이터베이스로 테스트하고,

Replace.ANY이면 기본적으로 내장된 임베디드 데이터베이스(예를 들어 H2 db)를 사용한다.

 

반응형
Comments