Spring test
์คํ๋ง ๋ถํธ์์๋ ๊ธฐ๋ณธ์ ์ธ ํ ์คํธ ์คํํฐ๋ฅผ ์ ๊ณตํ๋ค. ์คํํฐ์ ์ฌ๋งํ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋ญ์ณ๋์ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
spring-boot-test
spring-boot-test-autoconfigure
์ ๋๊ฐ ๋ชจ๋์ด ํ
์คํธ ๊ด๋ จ ์๋ ์ค์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ณ , ์ผ๋ฐ์ ์ผ๋ก spring-boot-starter-test
๋ก ๋ ๋ชจ๋์ ํจ๊ป ์ฌ์ฉํ๋ค.
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
๋จ์ ํ
์คํธ(JUnit)
Spring Boot์์ JUnit5๋ฅผ ์ด์ฉํด ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ธฐ ์ ์ ๊ธฐ๋ณธ์ ์ธ ๋ด์ฉ์ ๋ํด์ ๋ค๋ฃจ๊ณ ๋์ด๊ฐ ๊ฒ์ด๋ค. ์ผ๋ฐ์ ์ผ๋ก ๋จ์ ํ ์คํธ(Unit Test) ์ฝ๋๋ฅผ ์์ฑํ ๋ 5๊ฐ์ง ์์น์ ๊ฐ์กฐํ๋ค.
Fast : ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ๋ ์ผ์ ์ค๋ ๊ฑธ๋ฆฌ๋ฉด ์๋๋ค.
Independent : ๋ ๋ฆฝ์ ์ผ๋ก ์คํ์ด ๋์ด์ผํ๋ค.
Repeatable : ๋ฐ๋ณต ๊ฐ๋ฅํด์ผํ๋ค.
Self Validation : ๋ฉ๋ด์ผ ์์ด ํ ์คํธ ์ฝ๋๋ง ์คํํด๋ ์ฑ๊ณต, ์คํจ ์ฌ๋ถ๋ฅผ ์ ์ ์์ด์ผํ๋ค.
Timely : ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅํด์ผํ๋ค.
Junit
Junit์ Java์ ๋จ์ ํ ์คํ ๋๊ตฌ์ด๋ค.
๋จ์ ํ ์คํธ Framework์ค ํ๋
๋จ์ ๋ฌธ์ผ๋ก Test Case ์ํ๊ฒฐ๊ณผ๋ฅผ ํ๋ณํ๋ค.
Annotation์ผ๋ก ๊ฐ๊ฒฐํ๊ฒ ์ฌ์ฉ๊ฐ๋ฅํ๋ค.
Dependencies
spring boot 2.2.0 ์ดํ ๋ฒ์ ์์๋ Junit5๊ฐ ๊ธฐ๋ณธ์ผ๋ก ๋ณ๊ฒฝ๋์๋ค. Junit5๋ Java8 ๋ถํฐ ์ง์ํ๋ฉฐ, ์ด์ ๋ฒ์ ์ผ๋ก ์์ฑ๋ ํ ์คํธ ์ฝ๋์ฌ๋ ์ปดํ์ผ์ด ์ง์๋๋ค.
SpringBoot 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'
Test ๋จ์
@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
: ํ ์คํธ๋ฅผ ๋ง์น๊ณ ๋์ ์์ ๋ ๋ฐ์ดํฐ๊ฐ ๋กค๋ฐฑ๋๋ค.
Mock ๊ฐ์ฒด
ํ ์คํธ์ ๋ชจ๋ ๊ฐ๋ณ์ ์ธ ์์ญ(์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ, ๋ฉ์์ง ๋ฏธ๋ค์จ์ด, ๋ฐ์ดํฐ๋ฒ ์ด์ค)์ ๊ด๋ฆฌํ๊ธฐ ์ด๋ ค์ฐ๋ฉฐ, ์ํธ์์ฉ์ ๋จ์ ํ ์คํธ์ ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ค.
๋น์ฆ๋์ค ๋ก์ง์ ํ ์คํธํ ๋ชฉ์ ์ด๋ผ๋ฉฐ, ํด๋น ๋ก์ง์ด ์ํธ์์ฉํ๋ ๋ค์ํ ์์กด์ฑ์ ๋ชจ๋ ํ ์คํธํ ํ์๋ ์๋ค. Mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ํ ์คํธ ํ๊ฒฝ์์ ํ์ํ ์์กด์ฑ์ ๋์ฒดํ๊ณ , ์ธ๋ถ ์์กด์ฑ์ ์ํฅ ์์ด ๋น์ฆ๋์ค ๋ก์ง์ ์คํํ ์ ์๋ค.
Stub์ ํ ์คํธ์์ ์ฌ์ฉ๋๋ ํ๋์ฝ๋ฉ๋ ๊ตฌํ์ฒด์ด๋ค. (Stub์ Mock ๊ฐ์ฒด๊ฐ ์๋)
Mockito
Mockito๋ ํ๋ก์ ๊ธฐ๋ฐ์ Mock๊ฐ์ฒด ํ๋ ์์ํฌ๋ก ํํ๊ตฌ๋ฌธ๊ณผ ๋ง์ ์ ์ฐ์ฑ์ ์ ๊ณตํ๋ค. Mockito๋ฅผ ์ฌ์ฉํด ํ์ธ์ด ํ์ํ ๋์์ ๋ชจํนํด ์ค์ํ ๋์๋ง ๊ฒ์ฆํ ์ ์๋ค.
MockMvc Test
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 ๊ธฐ์ค์ผ๋ก ํ ์คํธ ์ํ
์ฐธ์กฐ
Last updated
Was this helpful?