본문 바로가기

스프링 부트

통합 테스트 VS 단위 테스트

2021-04-16글

통합 테스트

실제 운영 환경에서 사용될 클래스들을 통합하여 테스트한다.
스프링 프레임워크에서 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용한다.
여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트이다.

장점

  • 스프링 부트 컨테이너를 띄워 테스트하기 때문에 운영환경과 가장 유사한 테스트가 가능하다.
  • 전체적인 Flow를 쉽게 테스트 할 수 있다.

단점

  • 애플리케이션의 설정, 모든 Bean을 로드하기 때문에 시간이 오래걸리고 무겁다.
  • 테스트 단위가 커 디버깅이 어렵다.

@SpringBootTest

  • 통합 테스트를 제공하는 기본적인 스프링 부트 테스트 어노테이션.
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class ChessServiceImplTest {
  // ...
}

@SpringBootTest 의 파라미터들

  • value: 테스트가 실행되기 전에 적용할 프로퍼티 주입.(기존의 프로퍼티 오버라이드)
  • properties : 테스트가 실행되기 전에 {key=value} 형식으로 프로퍼티 추가.
  • classes : ApplicationContext에 로드할 클래스 지정. (지정하지 않으면 @SpringBootConfiguration을 찾아서 로드)
  • webEnvironment : 어플리케이션이 실행될 때의 웹 환경을 설정. (기본값은 Mock 서블릿을 로드하여 구동)

➕ webEnvironment

  • MOCK : ServletContainer를 테스트용으로 띄우지않고 서블릿을 mocking 한 것이 동작한다. (내장 톰캣이 구동되지 않는다.)
    MockMvc는 브라우저에서 요청과 응답을 의미하는 객체로서 Controller 테스테 사용을 용이하게 해주는 라이브러리
  • RANDOM_PORT : 임의의 Port Listener. EmbeddedWebApplicationContext를 로드하며 실제 서블릿 환경을 구성

@ActiveProfiles

원하는 프로파일 환경 값 설정이 가능하다. (프로파일 전략)

@Transactional

테스트 완료 후 자동으로 Rollback 처리가 된다.
하지만 WebEnvironment.RANDOM_PORT, DEFINED_PORT를 사용하면 실제 테스트 서버는 별도의 스레드에서 테스트를 수행하기 때문에 트랜잭션이 롤백되지 않는다. (왜?)

  • WebEnvironment.RANDOM_PORT, DEFINED_PORT 실제 웹 실행 환경을 띄운다.

단위 테스트

장점

  • WebApplication 관련된 Bean들만 등록하기 때문에 통합 테스트보다 빠르다.
  • 통합 테스트를 진행하기 어려운 테스트를 진행 가능하다.

단점

  • 요청부터 응답까지 모든 테스트를 Mock 기반으로 테스트하기 때문에 실제 환경에서는 제대로 동작하지 않을 수 있다.

@WebMvcTest

  • MVC를 위한 테스트로, 웹 상에서 요청과 응답에 대한 테스트.
  • MVC 관련된 설정인 @Controller, @ControllerAdvice, @JsonCompoent와 Filter, WebMvcConfiguer, HandlerMetohdAgumentResolver만 빈으로 등록된다. (디스패쳐 서블릿에서 사용되는 아이들만 주입받는다.)
  • 때문에 Service, Repository 와 같은 웹 계층 아래 빈들은 등록되지 않아 의존성도 끊긴다.
  • 테스트에 사용하는 의존성이 있다면 @MockBean으로 만들어 사용한다.
@WebMvcTest(ChessController.class)
class ChessControllerTest {
    private final static long CHESS_GAME_TEST_ID = 0;

    @Autowired
    MockMvc mockMvc;

    @MockBean
    ChessService chessService;

    @Test
    @DisplayName("게임 리스트 조회 테스트")
    void getGames() throws Exception {
        List<ChessGameManager> chessGameManagers = new ArrayList<>();
        ChessGameManager chessGameManager = ChessGameManagerFactory.createRunningGame(CHESS_GAME_TEST_ID);
        chessGameManagers.add(chessGameManager);

        given(chessService.findRunningGames()).willReturn(new ChessGameManagerBundle(chessGameManagers));

        mockMvc.perform(get("/games"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.runningGames." + CHESS_GAME_TEST_ID)
                        .value("WHITE"));
    }

    @Test
    @DisplayName("새로운 게임 시작 테스트")
    void gameStart() throws Exception {
        ChessGameManager chessGameManager = ChessGameManagerFactory.createRunningGame(CHESS_GAME_TEST_ID);

        given(chessService.start()).willReturn(chessGameManager);

        mockMvc.perform(get("/game/start"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(CHESS_GAME_TEST_ID))
                .andExpect(jsonPath("$.color").value("WHITE"))
                .andExpect(jsonPath("$.piecesAndPositions.size()").value(32));
    }

    @Test
    @DisplayName("게임 점수 조회 테스트")
    void getScore() throws Exception {
        ChessGameStatistics chessGameStatistics = ChessGameStatistics.createNotStartGameResult();

        given(chessService.getStatistics(CHESS_GAME_TEST_ID)).willReturn(chessGameStatistics);

        mockMvc.perform(get("/game/" + CHESS_GAME_TEST_ID + "/score"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.matchResult").value("무승부"))
                .andExpect(jsonPath("$.colorsScore.size()").value(2));
    }

    @Test
    @DisplayName("해당 게임 로딩 테스트")
    void loadGame() throws Exception {
        ChessGameManager chessGameManager = ChessGameManagerFactory.createRunningGame(CHESS_GAME_TEST_ID);

        given(chessService.findById(CHESS_GAME_TEST_ID)).willReturn(chessGameManager);

        mockMvc.perform(get("/game/" + CHESS_GAME_TEST_ID + "/load"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(CHESS_GAME_TEST_ID))
                .andExpect(jsonPath("$.color").value("WHITE"))
                .andExpect(jsonPath("$.piecesAndPositions.size()").value(32));
    }

    @Test
    void movePiece() throws Exception {
        Gson gson = new Gson();
        String content = gson.toJson(new MoveRequestDto("a2", "a3"));

        given(chessService.isEnd(CHESS_GAME_TEST_ID)).willReturn(false);
        given(chessService.nextColor(CHESS_GAME_TEST_ID)).willReturn(Color.BLACK);

        mockMvc.perform(MockMvcRequestBuilders
                .post("/game/" + CHESS_GAME_TEST_ID + "/move")
                .content(content).header("Content-Type", "application/json"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.end").value(false))
                .andExpect(jsonPath("$.nextColor").value("BLACK"));
    }
}
  • given - willReturn : 특정행위에 대한 반환 값을 지정하여 실제 객체처럼 동작하게 한다.

@JdbcTest

  • JDBC 기반 구성 요소에만 초점을 맞춘 JDBC 테스트 어노테이션
  • 테스트를 위한 JdbcTemplate이 생성된다.
  • 기본적으로 트랜잭션이 이루어진다.
  • in-memory database가 설정된다.
@JdbcTest
class JdbcTemplateChessDaoTest {
    private static final long DEFAULT_CHESS_GAME_ID = 1;
    ChessGame chessGame;
    ChessGameManager sampleGame;

    private final JdbcTemplate jdbcTemplate;
    private final JdbcTemplateChessDao jdbcTemplateChessDao;

    @Autowired
    public JdbcTemplateChessDaoTest(JdbcTemplate jdbcTemplate, @Qualifier("dataSource") DataSource dataSource) {
        this.jdbcTemplate = jdbcTemplate;
        this.jdbcTemplateChessDao = new JdbcTemplateChessDao(jdbcTemplate, dataSource);
    }

    @BeforeEach
    void beforeEach() {
        String sample = "RKBQKBKRPPPPPPPP................................pppppppprkbqkbkr"; // move a2 a3 한 번 진행
        chessGame = new ChessGame(DEFAULT_CHESS_GAME_ID, WHITE, true, sample);
        sampleGame = ChessGameManagerFactory.loadingGame(chessGame);
    }

    @Test
    @DisplayName("체스 게임을 저장한다.")
    void save() {
        long newId = jdbcTemplateChessDao.save(chessGame);
        assertThat(newId).isEqualTo(2);
    }

    @Test
    @DisplayName("id로 체스 게임을 찾는다.")
    void findById() {
        assertThat(jdbcTemplateChessDao.findById(DEFAULT_CHESS_GAME_ID).isPresent()).isTrue();
    }

    @Test
    @DisplayName("체스 게임 정보를 업데이트한다.")
    void update() {
        sampleGame.move(Position.of("a2"), Position.of("a4"));

        jdbcTemplateChessDao.update(new ChessGame(sampleGame));

        ChessGame expectedChessGame = jdbcTemplateChessDao.findById(DEFAULT_CHESS_GAME_ID).get();
        ChessGameManager expectedChessGameManager = ChessGameManagerFactory.loadingGame(expectedChessGame);
        Square a4 = expectedChessGameManager.getBoard().findByPosition(Position.of("a4"));
        assertThat(a4.getPiece().getClass()).isEqualTo(Pawn.class);
        assertThat(a4.getPiece().getColor()).isEqualTo(WHITE);
    }

    @Test
    void findAllOnRunning() {
        List<ChessGame> allOnRunning = jdbcTemplateChessDao.findAllOnRunning();

        assertThat(allOnRunning.size()).isEqualTo(1);
    }

    @Test
    void delete() {
        //when
        jdbcTemplateChessDao.delete(DEFAULT_CHESS_GAME_ID);

        //then
        try {
            jdbcTemplateChessDao.findById(DEFAULT_CHESS_GAME_ID);
        } catch (Exception e) {
        }
    }
}

참고