통합 테스트를 제공
- 실제 구동되는 어플리케이션과 똑같이 ApplicationContext를 로드하여 테스트
- 하고 싶은 테스트를 모두 수행 가능
- 어플리케이션에 설정된 bean을 모두 로드하기 때문에 규모가 클수록 느리다. (단위 테스트가 무의미해짐)
- 어플리케이션이 실행될 때의 설정을 임의로 바꾸어 테스트 진행 가능
- 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합
- 메인클래스와 함께 기본 제공
@RunWith(SpringRunner.class)
@SpringBootTestpublicclassDemoApplicationTests {
@TestpublicvoidcontextLoads() {
}
}- JUnit에 내장된 Runner를 사용하는 대신 어노테이션에 정의된 Runner 클래스 사용
- @SpringBootTest 를 사용하려면 JUnit 실행에 필요한 SpringJUnit4ClassRunner 클래스를 상속받은 @RunWith(SpringRunner.class)를 붙여야 한다.
value : 테스트가 실행되기 전에 적용할 프로퍼티 주입.(기존의 프로퍼티 오버라이드)
properties : 테스트가 실행되기 전에 {key=value} 형식으로 프로퍼티 추가.
classes : ApplicationContext에 로드할 클래스 지정. (지정하지 않으면 @SpringBootConfiguration을 찾아서 로드)
webEnvironment : 어플리케이션이 실행될 때의 웹 환경을 설정. (기본값은 Mock 서블릿을 로드하여 구동)
// value와 properties는 함께 사용할 수 없으므로 에러 발생@RunWith(SpringRunner.class)
@SpringBootTest(value = "value=test"
, properties = {"property.value=propertyTest"}
, classes = {DemoApplicationTests.class}
, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
publicclassDemoApplicationTests {
@Value("${value}")
privateStringvalue;
@Value("${property.value}")
privateStringpropertyValue;
@TestpublicvoidcontextLoads() {
assertThat(value, is("test"));
assertThat(propertyValue, is("propertyTest"));
}
}- 프로파일(개발, QA, 운영) 마다 다른 DataSource를 갖는 경우,
@ActiveProfiles("local")을 사용 @Transactional을 사용하면 테스트를 마치고 나서 수정된 데이터가 롤백된다.- @SpringBootTest는 기본적으로 @SpringBootApplication 이나 @SpringBootConfiguration 을 찾는다. (둘 중 하나는 필수)
MVC를 위한 테스트
- 웹에서 테스트하기 힘든 Controller를 테스트하는데 적합
- 웹상에서 요청과 응답에 대해 테스트할 수 있다.
- Security와 Filter까지 자동으로 테스트하며, 수동으로 추가/삭제 가능
- @WebMvcTest 를 사용하면 MVC 관련 설정들만 로드되기 때문에 가볍게 테스트 가능
- MVC 관련 설정:
@Controller,@ControllerAdvice,@JsonComponent,Filter,WebMvcConfigurer,HandlerMethodArgumentResolver
- MVC 관련 설정:
BookController에서 책 리스트를 받아오는 테스트
@NoArgsConstructor@GetterpublicclassBook {
privateIntegeridx;
privateStringtitle;
privateLocalDateTimepublishedAt;
@BuilderpublicBook(Stringtitle, LocalDateTimepublishedAt) {
this.title = title;
this.publishedAt = publishedAt;
}
}@ControllerpublicclassBookController {
@AutowiredprivateBookServicebookService;
@GetMapping("/books")
publicStringgetBookList(Modelmodel) {
model.addAttribute("bookList", bookService.getBookList());
return"book";
}
}이 인터페이스를 구현하는 구현체는 만들지 않고, Mock 데이터를 이용하여 테스트 진행
publicinterfaceBookService {
List<Book> getBookList();
}@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class)
publicclassBookControllerTest {
@AutowiredprivateMockMvcmvc;
@MockBeanprivateBookServicebookService;
@TestpublicvoidBook_MVC_TEST() throwsException {
Bookbook = newBook("Spring Boot Book", LocalDateTime.now());
given(bookService.getBookList()).willReturn(Collections.singletonList(book));
mvc.perform(get("/books"))
.andExpect(status().isOk()) // HTTP 상태값이 200인지 테스트
.andExpect(view().name("book")) // 반환되는 view의 이름이 book인지 테스트
.andExpect(model().attributeExists("bookList")) // Model의 프로퍼티 중 bookList라는 프로퍼티가 존재하는지 테스트
.andExpect(model().attribute("bookList", contains(book))); // Model의 프로퍼티 중 bookList 프로퍼티에 book 객체가 담겨져 있는지 테스트
}
}- @WebMvcTest를 사용하기 위해서는 테스트할 컨트롤러 이름을 명시해야 한다. (여기서는 BookContoller)
- MockMvc는 모든 의존성을 로드하지 않고 BookContoller와 관련된 bean만 로드한다.
- 여기서는 MockMvc를 주입시켰기 때문에 전체 HTTP서버를 실행하지 않고 테스트 가능
- BookService를 구현한 구현체는 없지만 @MockBean으로 BookService를 가짜객체로 대체함
- @DataJpaTest 는 JPA 테스트가 끝날 때 마다 자동으로 사용된 데이터를 롤백
- EntityManager의 대체재로 만들어진 테스트용 TestEntityManager를 사용하여
persist,flush,find등의 기본적인 JPA 테스트 가능
JPA 관련 테스트 설정만 로드
- DataSource의 설정이 정상적인지 테스트
- JPA를 사용하여 데이터를 제대로 생성, 수정, 삭제하는지 테스트
- 인메모리 임베디드 데이터베이스 사용
- 메인 메모리를 데이터 저장소로 하여 DB를 어플리케이션에 내장하여 운용하는 DB
- @Entity 클래스를 스캔하여 Spring Data JPA Repositories를 구성
기본 설정된 DataSource를 사용하지 않도록 아래와 같이 설정
@RunWith(SpringRunner.class)
@DataJpaTest@ActiveProfiles("...")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
publicclassJpaTest {
...
}또는 application.yml 파일을 아래와 같이 수정
spring.test.database.replace: NONEspring.test.database.connection: H2@AutoConfigureTestDatabase(connection = H2)
...@NoArgsConstructor@Getter@Entity@TablepublicclassBook {
@Id@GeneratedValueprivateIntegeridx;
@ColumnprivateStringtitle;
@ColumnprivateLocalDateTimepublishedAt;
@BuilderpublicBook(Stringtitle, LocalDateTimepublishedAt) {
this.title = title;
this.publishedAt = publishedAt;
}
}publicinterfaceBookRepositoryextendsJpaRepository<Book, Integer> {
}@RunWith(SpringRunner.class)
@DataJpaTestpublicclassBookJpaTest {
privatefinalstaticStringBOOT_TEST_TITLE = "Spring Boot Test Book";
@AutowiredprivateTestEntityManagertestEntityManager;
@AutowiredprivateBookRepositorybookRepository;
@TestpublicvoidBookList_Save_Test() {
Bookbook = Book.builder().title(BOOT_TEST_TITLE)
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book);
assertThat(bookRepository.getOne(book.getIdx()), is(book));
}
@TestpublicvoidBookList_Save_Search_Test() {
Bookbook1 = Book.builder().title(BOOT_TEST_TITLE + "1")
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Bookbook2 = Book.builder().title(BOOT_TEST_TITLE + "2")
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
Bookbook3 = Book.builder().title(BOOT_TEST_TITLE + "3")
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book3);
List<Book> bookList = bookRepository.findAll();
assertThat(bookList, hasSize(3));
assertThat(bookList, contains(book1, book2, book3));
}
@TestpublicvoidBookList_Save_Delete_Test() {
Bookbook1 = Book.builder().title(BOOT_TEST_TITLE + "1")
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Bookbook2 = Book.builder().title(BOOT_TEST_TITLE + "2")
.publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
bookRepository.deleteAll();
assertThat(bookRepository.findAll(), IsEmptyCollection.empty());
}
}REST 통신의 데이터형으로 사용되는 JSON 형식이 예상대로 응답을 반환하는지 등을 테스트
@RestControllerpublicclassBookRestController {
@AutowiredprivateBookRestServicebookRestService;
@GetMapping(path = "/rest/test", produces = MediaType.APPLICATION_JSON_VALUE)
publicBookgetRestBooks() {
returnbookRestService.getRestBook();
}
}getRestBook() 메서드의 반환값은 Book 객체이지만 @RestController로 설정되어 있으면 JSON 형식의 String형으로 반환된다.
@ServicepublicclassBookRestService {
privatefinalRestTemplaterestTemplate;
publicBookRestService(RestTemplateBuilderrestTemplateBuilder) {
this.restTemplate = restTemplateBuilder.rootUri("/rest/test").build();
}
publicBookgetRestBook() {
returnthis.restTemplate.getForObject("/rest/test", Book.class);
}
}@RunWith(SpringRunner.class)
@RestClientTest(BookRestService.class)
publicclassBookRestTest {
@RulepublicExpectedExceptionthrown = ExpectedException.none();
@AutowiredprivateBookRestServicebookRestService;
@AutowiredprivateMockRestServiceServerserver;
@Testpublicvoidrest_test() {
this.server.expect(requestTo("/rest/test"))
.andRespond(withSuccess(newClassPathResource("/test.json", getClass())
, MediaType.APPLICATION_JSON));
Bookbook = this.bookRestService.getRestBook();
assertThat(book.getTitle()).isEqualTo("테스트");
}
@Testpublicvoidrest_error_test() {
this.server.expect(requestTo("/rest/test"))
.andRespond(withServerError());
this.thrown.expect(HttpServerErrorException.class);
this.bookRestService.getRestBook();
}
}{"idx":null,"title":"테스트","publishedAt":null}JSON의 직렬화(Serialization)와 역직렬화(Deserialization)를 수행하는 라이브러리인 Gson과 Jackson API의 테스트를 제공
JSON 테스트는 두 가지로 나뉜다. 문자열로 나열된 JSON 데이터를 객체로 변환하여 변환된 객체값을 테스트하거나 그 반대.
@RunWith(SpringRunner.class)
@JsonTestpublicclassBookJsonTest {
@AutowiredprivateJacksonTester<Book> json;
@Testpublicvoidjson_test() throwsException {
Bookbook = Book.builder()
.title("테스트")
.build();
Stringcontent = "{\"title\":\"테스트\"}";
assertThat(this.json.parseObject(content).getTitle()).isEqualTo(book.getTitle());
assertThat(this.json.parseObject(content).getPublishedAt()).isNull();
assertThat(this.json.write(book)).isEqualToJson("/test.json");
assertThat(this.json.write(book)).hasJsonPathStringValue("title");
assertThat(this.json.write(book)).extractingJsonPathStringValue("title").isEqualTo("테스트");
}
}- <처음 배우는 스프링부트 2> 김영재 저