스프링 부트 테스트
스프링 부트는 기본적인 테스트 스타터 spring-boot-starter-test 를 제공. 크게 2가지 모듈.
이 장에서 알아볼 @Test
1) @SpringBootTest
- 특징과 쓰이는 때,
- 통합 테스트를 제공
- 여러가지 단위 테스트를 하나의 통합된 테스트로 수행할 때
- 만능이긴 한데, 애플리케이션에 설정된 Bean을 모두 로드하여 규모가 크면 느려짐.
- 단위 테스트하기엔 부적합해 질 수 있다.
@RunWith(SpringRunner.class)
@SpringBootTest(value = "value=test",
//properties = {"property.value=propertyTest"}, value랑 같이 사용 불가
classes = {SpringBootTestApplication.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringBootTestApplicationTests {
@Value("${value}")
private String value;
@Value("${property.value}")
private String propertyValue;
@Test
public void contextLoads() {
assertThat(value, is("test"));
assertThat(propertyValue, is("propertyTest"));
}
}
@RunWith(SpringRunner.class)
를 사용하면 JUnit에 내장된 러너 대신 정의된(SpringRunner.class) 러너 클래스를 사용한다.@SpringBootTest
는 JUnit 실행에 필요한 SpringJUnit4ClassRunner
클래스를 상속 받은 @RunWith
가 필요함.@SpringBootTest
의 프로퍼티와 사용법value = "value=test" --> @Value("${value}")
: 테스트 실행 전 적용할 프로퍼티를 주입, 기존 프로퍼티 오버라이드properties = {"property.value=propertyTest"} --> @Value("${property.value}")
: 테스트 실행 전 {key = value} 형식으로 프로퍼티 추가classes = {SpringBootTestApplication.class}
ApplicationContext에 로드할 클래스 지정. 지정 안할 시 @SpringBootConfiguration을 찾아서 로드webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
실행될 때의 웹 환경을 설정, 기본값은 Mock서블릿 로드해서 구동.@SpringBootTest
사용시 Tip@ActiveProfiles("local")[dev, real 등..]
처럼 프로파일 설정 가능 (DataSource 등등에 사용)테스트
에서 @Transactional
사용시 테스트 마치고 Rollback함.@SpringBootApplication
이나 @SpringBootConfiguration
어노테이션 중 하나는 필수2) @WebMvcTest
- 주로 Controller를 테스트 할 때. ( 요청 / 응답 )
- 시큐리티, 필터까지 자동으로 테스트
@Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver
만 로드되어 가볍다.@NoArgsConstructor // 기본 생성자
@Getter
public class Book {
@Id
@GeneratedValue
private Integer idx;
@Column
private String title;
@Column
private LocalDateTime publishedAt;
@Builder
public Book(String title, LocalDateTime publishedAt) {
this.title = title;
this.publishedAt = publishedAt;
}
}
@Controller
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/books")
public String getBookList(Model model) {
model.addAttribute("bookList", bookService.getBookList());
return "book";
}
}
public interface BookService {
List<Book> getBookList();
}
// 구현체는 만들지 않고 Mock 데이터로 테스트 함.
@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class) // 테스트에 사용할 클래스 명시
public class BookControllerTest {
// 모든 의존성이 아닌 BookController 관련 Bean 만 로드. 여기서는 HTTP 서버를 대신 하려고.
@Autowired
private MockMvc mvc;
// 구현체는 없지만 Mock(목) 가짜 객체로 쓸 것임.
@MockBean
private BookService bookService;
@Test
public void Book_MVC_테스트() throws Exception {
Book book = new Book("Spring Boot Book", LocalDateTime.now());
given(bookService.getBookList()).willReturn(Collections.singletonList(book));
//given( 어떤 메서드를 주면).willReturn( XXX가 return 될거다 )
mvc.perform(get("/books"))
.andExpect(status().isOk()) // HTTP status 200
.andExpect(view().name("book")) // 반환 view 이름이 'book'
.andExpect(model().attributeExists("bookList")) // model 프로퍼티 중 'bookList' 존재하는지
.andExpect(model().attribute("bookList", contains(book))); // 해당 프로퍼티에 book 객체가 담겨 있는지
}
}
3) @DataJpaTest
- 특징과 쓰이는 때,
- JPA 관련 테스트 설정만 로드
- 데이터 소스의 설정, JPA로 데이터를 생성, 수정, 삭제 정상적으로 하는지
- 내장형 데이터베이스 (H2) 사용 등
@Entity
클래스를 스캔하여 JPA Repositories 구성
별도의 DataSource 를 사용하도록 Profile 사용
@RunWith(SpringRunner.class)
@DataJpaTest
//dev 프로파일 설정값으로 사용
@ActiveProfiles("dev")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
//.Any는 기본 내장 DataSource 사용
//@AutoConfigureTestDatabase(connection = H2)
public class BookJpaTest {
.....
}
@DataJpaTest
는 자동으로 테스트 후 Rollback@NoArgsConstructor
@Getter
@Entity // 테이블과 매핑되어 JPA가 관리할 클래스
@Table // 매핑할 테이블 지정, 생략시 엔티티 네임으로 테이블 매핑
public class Book {
@Id
@GeneratedValue
private Integer idx;
@Column
private String title;
@Column
private LocalDateTime publishedAt;
@Builder
public Book(String title, LocalDateTime publishedAt) {
this.title = title;
this.publishedAt = publishedAt;
}
public interface BookRepository extends JpaRepository<Book, Integer> { //Q. Integer는 PK?
}
@RunWith(SpringRunner.class)
@DataJpaTest //자동으로 Rollback
public class BookJpaTest {
private final static String BOOT_TEST_TITLE = "Spring Boot Test Book";
@Autowired
private TestEntityManager testEntityManager; // 테스트용 EntityManager
@Autowired
private BookRepository bookRepository;
@Test
public void Book저장하기_테스트() {
Book book = Book.builder().title(BOOT_TEST_TITLE).publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book); // persist 테스트
assertThat(bookRepository.getOne(book.getIdx()), is(book));
}
// book 3개 저장하고 3개 맞는지, 각각 맞는 객체인지
@Test
public void BookList저장하고_찾기_테스트() {
Book book1 = Book.builder().title(BOOT_TEST_TITLE+"1").publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Book book2 = Book.builder().title(BOOT_TEST_TITLE+"2").publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
Book book3 = 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));
}
// 2개 잘 삭제 됐는지.
@Test
public void BookList저장하고_삭제_테스트() {
Book book1 = Book.builder().title(BOOT_TEST_TITLE+"1").publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book1);
Book book2 = Book.builder().title(BOOT_TEST_TITLE+"2").publishedAt(LocalDateTime.now()).build();
testEntityManager.persist(book2);
bookRepository.deleteAll();
assertThat(bookRepository.findAll(), IsEmptyCollection.empty());
}
// Q. 클래스 전체를 한 번에 테스트하면 (RUN - public class BookJpaTest), 서로 영향이 없나?
// 클래스 전체 테스트 시 메서드의 순서는 보장되지 않음.
// 인프런 - 김영한 강의 중
// @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.
// 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.
}
BookRestController 실제 컨트롤러4) @RestClientTest
- Rest 통신에서 JSON으로 잘 응답하는지 등등
@RestController
public class BookRestController {
@Autowired
private BookRestService bookRestService;
@GetMapping(path = "/rest/test", produces = MediaType.APPLICATION_JSON_VALUE)//application/json
public Book getRestBooks() {
return bookRestService.getRestBook(); //getRestBook()의 반환값은 Book인데 JSON형식 String으로 반환
}
}
@Service
public class BookRestService {
private final RestTemplate restTemplate;
// Constuctor
public BookRestService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.rootUri("/rest/test").build();
}
public Book getRestBook() {
return this.restTemplate.getForObject("/rest/test", Book.class);
}
}
@RunWith(SpringRunner.class)
@RestClientTest(BookRestService.class) // 테스트에 사용할 Bean 주입
public class BookRestTest {
@Rule // 테스트 메서드 종료시마다 정의한 값으로 초기화
public ExpectedException thrown = ExpectedException.none();
@Autowired
private BookRestService bookRestService;
@Autowired
private MockRestServiceServer server; // 서버를 대신할 용도
// 요청과 응답이 같은지만 테스트
@Test
public void rest_테스트() {
this.server.expect(requestTo("/rest/test")) //요청 URI
// 응답할 JSON 값 (json 파일을 읽어서)
.andRespond(withSuccess(new ClassPathResource("/test.json", getClass()), MediaType.APPLICATION_JSON));
Book book = this.bookRestService.getRestBook();
assertThat(book.getTitle()).isEqualTo("테스트");
}
/**
* test.json
* {"idx":null, "title":"테스트","publishedAt":null}
*
*/
@Test
public void rest_error_테스트() {
this.server.expect(requestTo("/rest/test"))
.andRespond(withServerError());
this.thrown.expect(HttpServerErrorException.class); // HTTP response status is 500
this.bookRestService.getRestBook();
}
}
5) @JsonTest
- Gson과 Jackson API 테스트 제공 ( 각각 -Tester)
- Jackson API로 테스트
JSON data -> 객체
or객체 -> JSON data
- 직렬화 / 역직렬화 BookJsonTest ( @JsonTest )
@RunWith(SpringRunner.class)
@JsonTest
public class BookJsonTest {
@Autowired
private JacksonTester<Book> json;
@Test
public void json_테스트() throws Exception { // Q. 왜 여기만 throws Exception 을 붙인것인가
Book book = Book.builder()
.title("테스트")
.build();
String content = "{\"title\":\"테스트\"}";
// String을 객체로 변환 ( Q. 객체의 Type은 그냥 Java Object ? )
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("테스트");
}
}
마치며
Spring의 모든 Bean을 올리지 말고, 각 테스트에 필요한 가짜 객체를 잘 활용할 것!