스프링 부트에서는 기본적인 테스트 스타터를 제공한다. 스타터에 웬만한 테스트 라이브러리들을 뭉쳐놓아 편리하게 사용할 수 있다.
spring-boot-test
spring-boot-test-autoconfigure
위 두개 모듈이 테스트 관련 자동 설정 기능을 제공하고, 일반적으로 spring-boot-starter-test
로 두 모듈을 함께 사용한다.
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Spring Boot에서 JUnit5를 이용해 테스트 코드를 작성해보기 전에 기본적인 내용에 대해서 다루고 넘어갈 것이다. 일반적으로 단위 테스트(Unit Test) 코드를 작성할 때 5가지 원칙을 강조한다.
- Fast : 테스트 코드를 실행하는 일은 오래 걸리면 안된다.
- Independent : 독립적으로 실행이 되어야한다.
- Repeatable : 반복 가능해야한다.
- Self Validation : 메뉴얼 없이 테스트 코드만 실행해도 성공, 실패 여부를 알 수 있어야한다.
- Timely : 바로 사용 가능해야한다.
Junit은 Java의 단위 테스팅 도구이다.
- 단위 테스트 Framework중 하나
- 단정문으로 Test Case 수행결과를 판별한다.
- Annotation으로 간결하게 사용가능하다.
2.2.0 이후 버전에서는 Junit5가 기본으로 변경되었다.
-
maven
<!-- spring boot test junit5 사용 exclusion을 통해 junit4에서 코드 실행시 사용하는 vintage-engine 예외처리--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!-- 테스트 코드 작성에 필요한 junit-jupiter-api 모듈과 테스트 실행을 위한 junit-jupiter-engine 모듈 포함 --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> </dependency>
-
gradle
testImplementation ('org.springframework.boot:spring-boot-starter-test') { exclude module: 'junit' } testImplementation 'org.junit.jupiter:junit-jupiter-api' testRuntimeOnly'org.junit.jupiter:junit-jupiter-engine'
설명 | Bean | |
---|---|---|
@SpringBootTest | 통합 테스트, 전체 | Bean 전체 |
@WebMvcTest | 단위 테스트, Mvc 테스트 | MVC 관련된 Bean |
@DataJpaTest | 단위 테스트, Jpa 테스트 | JPA 관련 Bean |
@RestClientTest | 단위 테스트, Rest API 테스트 | 일부 Bean |
@JsonTest | 단위 테스트, Json 테스트 | 일부 Bean |
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* ExtendWith : JUnit5 확장 기능
*
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = T2020AprilApplicationTests.class)
@ActiveProfiles("test")
@Transactional
public class T2020AprilApplicationTests {
@Test
void contextLoads() {
}
}
-
@SpringBootTest
는 통합 테스트를 제공하는 기본적인 Spring Boot Test 어노테이션이다. 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합하다.-
@SpringBootApplication
이 붙은 어노테이션을 찾아 context를 찾는다. -
실제 구동되는 애플리케이션과 똑같이 애플리케이션 컨텍스트를 로드해 테스트하기 때문에 하고 싶은 테스트를 모두 수행할 수 있다.
-
애플리케이션에서 설정된 빈을 모두 로드하기 때문에 애플리케이션 규모가 클수록 느려진다.
-
properties : 테스트 실행되기전 {key,value} 형식으로 프로퍼티를 추가할 수 있다.
@SpringBootTest(properties = {"property.value= propertyTest"})
-
value : 테스트 실행 전 적용할 프로퍼티를 주입할 수 있다.
@SpringBootTest(value= "value=test")
-
classes : 애플리케이션 컨텍스트에 로드할 클래스를 지정할 수 있다. 별도로 설정하지 않으면,
@SpringBootApplication
or@SpringBootConfiguration
을 찾아서 로드한다.@SpringBootTest(classes= {SpringBootTestApplication.class})
-
-
@ExtendWith
는 확장기능을 구현한다.- JUnit5에서 제공하는 기능의 상당수가 이 기능으로 지원되고 있다.
- 실제 기능이 해당 어노테이션을 통해서 실행된다.
- JUnit4에서 RunWith와 유사하다.
-
@ActiveProfiles
는 원하는 프로파일 환경 값을 부여할 수 있다. -
@Transactional
: 테스트를 마치고 나서 수정된 데이터가 롤백된다.
MockMvc 는 브라우저에서 요청과 응답을 의미하는 객체로서 웹에서 테스트하기 힘든 Controller 테스트를 용이하게 해준다. 또한 시큐리티 혹은 필터까지 자동으로 테스트해 수동으로 추가/삭제도 가능하다.
@WebMvcTest
로 테스트를 할 수 있다.
-
Book.java
package com.example.boot.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDateTime; @NoArgsConstructor @Getter public class Book { private Integer idx; private String title; private LocalDateTime publishedAt; @Builder public Book(String title, LocalDateTime publishedAt){ this.title = title; this.publishedAt = publishedAt; } }
-
Service
package com.example.boot.service; import com.example.boot.domain.Book; import java.util.List; public interface BookService { List<Book> getBookList(); }
-
Controller
package com.example.boot.controller; import com.example.boot.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class BookController { @Autowired private BookService bookService; @GetMapping("/books") public String getBookList(Model model) { model.addAttribute("bookList", bookService.getBookList()); return "book"; } }
-
test
package com.example.boot; import com.example.boot.controller.BookController; import com.example.boot.domain.Book; import com.example.boot.service.BookService; 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.hamcrest.Matchers.contains; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.time.LocalDateTime; import java.util.Collections; @WebMvcTest(BookController.class) public class BookControllerTest { @Autowired private MockMvc mockMvc; @MockBean private BookService bookService; @Test public void Book_MVC_TEST() throws Exception { Book book = new Book("Spring Boot Book", LocalDateTime.now()); given(bookService.getBookList()).willReturn(Collections.singletonList(book)); mockMvc.perform(get("/books")) .andExpect(status().isOk()) .andExpect(view().name("book")) .andExpect(model().attributeExists("bookList")) .andExpect(model().attribute("bookList",contains(book))); } }
-
@WebMvcTest(BookController.class)
: 테스트할 컨트롤러명을 명시해준다. -
여기서
@Service
은@WebMvcTest
의 적용 대상이 아니다.BookService
인터페이슬르 구현한 구현체는 없지만,@MockBean
을 적극적으로 활용해 컨트롤러 내부의 의존성 요소를 각자 객체로 대체 했다. MockBean은 실제 객체는 아니지만 실제 객체처럼 동작하게 만들 수 있다.given(bookService.getBookList()).willReturn(Collections.singletonList(book));
: 가짜 객체를 만들어given()
을 사용해 메서드 실행에 대한 반환값을 미리 설정했다.
-
그후
andExpect()
로 예상 값이 나오는지에 대해서 테스트를 진행했다.
만약 기본설정만 필요하다면 @AutoConfigureMockMvc
으로 간단하게 설정할 수 있다.
@SpringBootTest
@AutoConfigureMockMvc
public abstract class AbstractControllerTest {
@Autowired
protected MockMvc mockMvc;
}
하지만 AutoConfig로 설정을 하면 Custom하기 어려워진다. 아래 코드는 MockMvcBuilders를 활용해 MockMvc 인스턴스를 생성해주었다. 이렇게 공통 추상클래스를 만들어 사용할 수 있다.
/**
* 테스트시 필요한 커스텀 공통 설정 추상 클래스
*/
@SpringBootTest
public abstract class AbstractControllerTest {
protected MockMvc mockMvc;
abstract protected Object controller();
@BeforeEach
private void setup() {
mockMvc = MockMvcBuilders.standaloneSetup(controller()) // 기본설정
.addFilter(new CharacterEncodingFilter(StandardCharsets.UTF_8.name(), true)) // 테스트 수행시 한글 깨짐 방지
.alwaysDo(print()) // 항상 콘솔 출력
.build();
}
}
이제 추상 클래스를 상속받아 Controller 테스트를 수행할 수 있다.
/**
* 테스트시 필요한 공통 설정 추상 클래스(AbstractController) 상속
* PaymentGatewayController 테스트 클래스
* TestMethodOrder : OrderAnnotaion기준으로 테스트 메소드 수행
*/
@TestMethodOrder(OrderAnnotation.class)
public class PaymentGatewayControllerTest extends AbstractControllerTest {
private static String[] pmtCodeArr = {"P0001", "P0001", "P0002", "P0003", "P0003", "P0004", "P0005"}; // pmtCode 테스트 데이터 배열
private static String[] mbrIdArr = {"0000000345", "0000000911", "0000000602"}; // mbrId 테스트 데이터 배열
@Autowired
PaymentGatewayController paymentGatewayController;
/**
* @return 테스트할 paymentController 인스턴스
*/
@Override
protected Object controller() {
// TODO Auto-generated method stub
return paymentGatewayController;
}
/**
* Test method for {@link com.example.test.controller.PaymentGatewayController#approve(java.lang.String, java.lang.String, java.lang.String, long)}.
* 결제 승인 요청 테스트
* 각각 mbrId별로 pmtCodeArr 데이터로 생성
* 이떄, pmtType은 null로 보낸다.(자동으로 생성되도록)
* Order annotation은 테스트 실행순서 지정
*/
@Test
@Order(1)
void testApprove() {
try {
for(int i=0;i<7;i++) {
for(String mbrId : mbrIdArr) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("mbrId", mbrId);
params.add("pmtCode", pmtCodeArr[i]);
params.add("pmtType", "");
params.add("pmtAmt", "157400");
// curl -X POST "http://localhost:8080/api/pg/approve?mbrId=&pmtAmt=&pmtCode=" -H "accept: */*"
mockMvc.perform(post("/api/pg/approve")
.contentType(MediaType.APPLICATION_JSON)
.params(params))
.andExpect(status().isOk()); // 성공여부 확인
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* Test method for {@link com.example.test.controller.PaymentGatewayController#getRecentPaymentList(java.lang.String, java.lang.String, java.lang.Integer)}.
* 최근 결제내역리스트 조회 테스트
* 각각 member 별로 10개씩 조회
*/
@Test
@Order(2)
void testGetRecentPaymentList() {
try {
for(String mbrId : mbrIdArr) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("mbrId", mbrId);
params.add("size", "10");
// curl -X GET "http://localhost:8080/api/pg/approve?mbrId=&pmtAmt=&pmtCode=" -H "accept: */*"
mockMvc.perform(get("/api/pg/getRecentPaymentList")
.params(params))
.andExpect(status().isOk()) // 수행결과 확인
.andExpect(jsonPath("$[*]", hasSize(10))) // 각 멤버별 리스트 수 확인
.andExpect(jsonPath("$[?(@.succYn =='Y')]", hasSize(9))) // 성공여부 성공(Y) 9개 확인
.andExpect(jsonPath("$[?(@.succYn =='N')]", hasSize(1))); // 성공여부 실패(N) 1개 확인
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Test
: 테스트 대상을 지정@Order
: 테스트 수행 순서 지정@TestMethodOrder(OrderAnnotation.class)
: OrderAnnotation 기준으로 테스트 수행