์คํ๋ง ๋ถํธ์์๋ ๊ธฐ๋ณธ์ ์ธ ํ
์คํธ ์คํํฐ๋ฅผ ์ ๊ณตํ๋ค. ์คํํฐ์ ์ฌ๋งํ ํ
์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ๋ญ์ณ๋์ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋ค.
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 ๋จ์
๋จ์ ํ
์คํธ, Mvc ํ
์คํธ
๋จ์ ํ
์คํธ, Jpa ํ
์คํธ
๋จ์ ํ
์คํธ, Rest API ํ
์คํธ
๋จ์ ํ
์คํธ, Json ํ
์คํธ
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 ๊ธฐ์ค์ผ๋ก ํ
์คํธ ์ํ
์ฐธ์กฐ