Programming/Spring Boot

@SpringBootTest 스프링 부트 통합 테스트 개념과 예시 코드

Jan92 2023. 10. 31. 22:15
반응형

@SpringBootTest를 사용한 스프링 부트 통합 테스트 개념과 예시 코드

스프링 부트 통합 테스트 @SpringBootTest

 

Spring Boot 프로젝트의 테스트 코드 작성 방법에 대해 찾아보면서, 테스트 방식이 크게 '@SpringBootTest를 사용하는 통합 테스트''@WebMvcTest, @DataJpaTest 등의 어노테이션을 사용하는 슬라이스 테스트'로 나뉜다는 것을 알게 되었습니다.

(슬라이스 테스트에 사용되는 어노테이션 @WebMvcTest, @WebFluxTest, @DataJpaTest, @JsonTest, @RestClientTest 등)

 

해당 포스팅에서는 '@SpringBootTest를 사용하는 통합 테스트의 개념과 장단점, 예시 코드'에 대해서 살펴보겠습니다.

 

* 전체 코드는 포스팅 맨 하단에 링크해 두었으니 필요하신 경우 참고하시면 좋을 것 같습니다.

 

 


테스트에 필요한 의존성

dependencies {
	...
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

스프링 부트 애플리케이션의 테스트 코드를 작성하기 위해서는 먼저 위 'spring-boot-starter-test' 의존성 추가가 필요한데요.

대부분 프로젝트 생성 시 자동으로 들어가기 때문에 따로 추가해줘야 하는 경우는 잘 없던 것으로 알고 있습니다.

 

 


@SpringBootTest

앞서 이야기한 것처럼 '@SpringBootTest' 어노테이션을 사용하면 통합 테스트를 위한 환경을 쉽게 구축할 수 있는데요.

 

구체적으로 @SpringBootTest프로젝트 내부에 있는 모든 스프링 빈들을 스캔하여 등록하고, 애플리케이션 컨텍스트를 생성하여 테스트를 실행합니다.

애플리케이션의 설정과 모든 Bean들을 로드하기 때문에 운영환경과 가장 유사한 테스트가 가능하다는 장점이 있으며, 이를 통해 전체적인 Flow가 제대로 동작하는지 검증할 수 있습니다.

 

단점으로는 애플리케이션의 설정과 모든 빈들을 스캔하여 등록하기 때문에 동작 시간이 오래 걸리고 무겁다는 점이 있습니다.

또한 테스트의 단위가 크기(전체적인 Flow) 때문에 디버깅이 다소 어려울 수 있습니다.

(이러한 이유로 인해 특정 부분한 테스트하는 슬라이스 테스트를 구현하게 되는데요. 슬라이스 테스트에 관련된 내용은 이어지는 포스팅에서 따로 정리하도록 하겠습니다.)

 

 


@SpringBootTest 속성

아래 내용을 통해서 @SpringBootTest 어노테이션에 적용할 수 있는 속성에 대해 살펴보겠습니다.

 

@SpringBootTest(properties = {"key1=value1", "key2=value2"})
@SpringBootTest(value = {"key1=value1", "key2=value2"})
@SpringBootTest({"key1=value1", "key2=value2"})

먼저 'properties' 속성입니다. properties 속성을 통해 속성 값을 정의할 수 있는데요.

예시와 같이 properties 또는 value 또는 생략하고 입력하여도 동일하게 동작합니다.

 

 

@SpringBootTest(classes = {BoardApiController.class})

이어서 'classes' 속성입니다.

@SpringBootTest는 프로젝트의 모든 빈을 등록하는데요. 하지만 classes 속성을 정의하면 해당 클래스만 빈으로 등록되며, 이때 @Configuration으로 지정한 설정도 등록할 수 있습니다.

 

 

@SpringBootTest(
        properties = {"key1=value1", "key2=value2"},
        args = {"--secret=exampleKey"}
)
class BoardApplicationTests {

    @Value("${key1}")
    private String key1;

    @Value("${key2}")
    private String key2;

    @Autowired
    private ApplicationArguments args;

    @BeforeEach
    void before() {
        String secret = args.getOptionValues("secret").get(0);
    }
}

(properties, args 속성을 사용하는 예시)

 

 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)

'webEnvironment' 속성입니다.

 

 

WebEnvironment.MOCK

WebEnvironment 설정의 기본값입니다.

웹 기반의 애플리케이션 컨텍스트를 생성하지만 MOCK 환경으로 제공하기 때문에 내장 서버가 실행되지 않으며, Mock 테스트를 위해 @AutoConfigureMockMvc 또는 @AutoConfigureWebTestClient를 함께 사용할 수 있습니다.

 

WebEnvironment.RANDOM_PORT

웹 기반의 애플리케이션 컨텍스트를 생성하여 실제 서블릿 환경을 제공합니다.

내장 서버도 실행되며, 이때 사용하지 않는 랜덤 포트를 listen 합니다.

 

WebEnvironment.DEFINED_PORT

RANDOM_PORT와 마찬가지로 웹 기반의 애플리케이션 컨텍스트를 생성하고 실제 서블릿 환경을 제공합니다.

내장 서버도 실행되며, 이때 설정을 통해 지정된 포트를 listen 합니다.

 

WebEnvironment.NONE

기본적인 애플리케이션 컨텍스트를 로드하며, 서블릿 환경을 제공하지 않습니다.

 

 

***

추가로 JUnit 4를 사용하는 경우 '@RunWith(SpringRunner.class)' 어노테이션을 추가로 붙여주어야 테스트가 정상적으로 동작합니다.

 

***

추가로 테스트 동작 시 '@ActiveProfiles' 어노테이션을 통해 원하는 프로파일 환경 값을 설정할 수 있는데요.

아래 예시에서도 @ActiveProfiles("local")을 통해 application-local.yml 설정 값을 사용하였습니다.

 

 


@Transactional

@Test 어노테이션과 @Transactional 어노테이션을 함께 사용하면, 테스트 완료 후 해당 테스트 내용을 자동으로 rollback 처리하게 되는데요.

하지만 WebEnvironment 설정값으로 'RANDOM_PORT' 또는 'DEFINED_PORT'를 사용하는 경우 실제 서블릿 환경이 제공되기 때문에 HTTP Client와 서버는 별도의 스레드에서 실행되며, 별도의 트랜잭션에서 동작합니다.

때문에 이 경우 서버에서 시작된 트랜잭션은 rollback 되지 않는다는 특징이 있습니다.

 

 


테스트 예시 코드

* 아직 실무적으로는 테스트 코드를 거의 짜보지 못했기 때문에 아래 예시 코드는 MockMvc, TestRestTemplate 등을 사용하여 이렇게 테스트가 동작할 수 있구나 정도만 참고해 주시면 좋을 것 같습니다.

 

 

MockMvc를 사용하는 테스트 코드 예시

@Transactional
@AutoConfigureMockMvc
@ActiveProfiles("local")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class BoardApplicationTests {

	@Autowired
	private MockMvc mockMvc;
	@Autowired
	private ObjectMapper objectMapper;
	@Autowired
	private WebApplicationContext ctx;

	@Autowired
	private BoardService boardService;
	@Autowired
	private BoardRepository boardRepository;

	@DisplayName("게시글 조회 테스트 with MockMvc")
	@Test
	void getBoardTestWithMockMvc() throws Exception {
		String requestTitle = "제목";
		String requestContent = "내용";
		BoardRegisterRequest boardRegisterRequest = new BoardRegisterRequest(requestTitle, requestContent);
		BoardResponse savedBoardResponse = boardService.registerBoard(boardRegisterRequest);

		mockMvc.perform(get("/board/" + savedBoardResponse.getId()))
				.andExpect(status().isOk())
				.andExpect(content().contentType(MediaType.APPLICATION_JSON))
				.andExpect(jsonPath("$.title", is(requestTitle)))
				.andDo(print());
	}

	@DisplayName("게시글 등록 테스트 with MockMvc")
	@Test
	void registerBoardTestWithMockMvc() throws Exception {
		String requestTitle = "제목";
		String requestContent = "내용";
		BoardRegisterRequest boardRegisterRequest = new BoardRegisterRequest(requestTitle, requestContent);

		String content = objectMapper.writeValueAsString(boardRegisterRequest);

		mockMvc.perform(post("/board")
				.content(content)
				.contentType(MediaType.APPLICATION_JSON)
				.accept(MediaType.APPLICATION_JSON))
				.andExpect(status().isOk())
				.andDo(print());
	}
}

@AutoConfigureMockMvc 어노테이션을 추가하고 @Autowired를 통해 MockMvc를 주입받으면 다음과 같이 MockMvc를 간단하게 사용할 수 있는데요.

 

MockMvc를 사용한 다음과 같은 테스트 코드의 경우 WebEnvironment 설정이 MOCK, DEFINED_PORT, RANDOM_PORT 모두 테스트가 정상적으로 동작했으며, 테스트 종료 후 트랜잭션이 롤백되는 것을 확인하였습니다.

 

 

TestRestTemplate을 사용하는 테스트 코드 예시

@Transactional
@AutoConfigureMockMvc
@ActiveProfiles("local")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BoardApplicationTests {

	@Autowired
	private TestRestTemplate testRestTemplate;
	@LocalServerPort
	private int port;

	@Autowired
	private BoardService boardService;
	@Autowired
	private BoardRepository boardRepository;

	//rollback 안됨
	@DisplayName("게시글 조회 테스트 with TestRestTemplate")
	@Test
	void getBoardTestWithTestRestTemplate() {
		String requestTitle = "제목";
		String requestContent = "내용";
		BoardRegisterRequest boardRegisterRequest = new BoardRegisterRequest(requestTitle, requestContent);

		String registerUrl = "http://localhost:" + this.port + "/board";

		ResponseEntity<BoardResponse> responseEntity
				= testRestTemplate.postForEntity(registerUrl, boardRegisterRequest, BoardResponse.class);

		String findUrl = "http://localhost:" + this.port + "/board/" + responseEntity.getBody().getId();

		ResponseEntity<BoardResponse> response =
				testRestTemplate.getForEntity(findUrl, BoardResponse.class);
		then(response.getStatusCode()).isEqualTo(HttpStatus.OK);
		then(response.getBody()).isNotNull();
	}

	//rollback 안됨
	@DisplayName("게시글 등록 테스트 with TestRestTemplate")
	@Test
	void registerBoardTestWithTestRestTemplate() {
		String requestTitle = "제목";
		String requestContent = "내용";
		BoardRegisterRequest boardRegisterRequest = new BoardRegisterRequest(requestTitle, requestContent);

		String url = "http://localhost:" + this.port + "/board";

		ResponseEntity<BoardResponse> responseEntity
				= testRestTemplate.postForEntity(url, boardRegisterRequest, BoardResponse.class);

                Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                Assertions.assertThat(responseEntity.getBody().getTitle()).isEqualTo(boardRegisterRequest.getTitle());
	}
}

TestRestTemplate은 RestTemplate의 테스트를 위한 버전인데요.

TestRestTemplate을 사용하게 되면 클라이언트의 관점에서 요청하는 것과 같은 테스트를 진행할 수 있습니다.

 

TestRestTemplate은 WebEnvironment의 설정에 따라 자동으로 빈이 생성되는데요.

DEFINED_PORT, RANDOM_PORT 설정에서 정상적으로 동작하였으며, 테스트 종료 후 트랜잭션은 롤백되지 않았습니다.

 

 

MockMvc, TestRestTemplate 둘 다 사용하지 않는 테스트 코드 예시

@Transactional
@ActiveProfiles("local")
@SpringBootTest
class BoardApplicationTests {

	@Autowired
	private BoardService boardService;
	@Autowired
	private BoardRepository boardRepository;

	@DisplayName("게시글 등록 테스트")
	@Test
	void registerBoardTest() {
		String requestTitle = "제목";
		String requestContent = "내용";
		BoardRegisterRequest boardRegisterRequest = new BoardRegisterRequest(requestTitle, requestContent);

		BoardResponse boardResponse = boardService.registerBoard(boardRegisterRequest);

		Assertions.assertThat(requestTitle).isEqualTo(boardResponse.getTitle());
		Assertions.assertThat(requestContent).isEqualTo(boardResponse.getContent());
	}


	@DisplayName("게시글 전체 조회 테스트")
	@Test
	void getBoardListTest() {
		String title = "제목";
		String content = "내용";
		List<Board> boardList = new ArrayList<>();
		int count = 5;
		for (int i=0; i<count; i++) {
			boardList.add(Board.builder().title(title + i).content(content + i).build());
		}
		boardRepository.saveAll(boardList);

		List<BoardResponse> boardResponseList = boardService.getBoardList();

		Assertions.assertThat(boardResponseList.size()).isEqualTo(count);
	}
}

 

 

< Github 전체 코드 >
https://github.com/JianChoi-Kor/SpringBootTest/tree/master


< 참고 자료 >
https://goddaehee.tistory.com/211
https://cheolhojung.github.io/posts/java/springboot-test.html
https://www.inflearn.com/blogs/339

반응형