JiwonDev

Spring Boot Test์™€ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ

by JiwonDev

2022.03.06 SpringBootTest ๋‚ด์šฉ ์ถ”๊ฐ€(์ถ”ํ›„ ์ •๋ฆฌ ์˜ˆ์ •)

 

System.out ๊ฐ™์€ ๊ฑธ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ๋ชปํ•˜์ž–์•„์š”?
-> ์ธํ„ฐํŽ˜์ด์Šค๋กœ ์ถ”์ƒํ™”ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ , ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌ์‹œํ‚ค๋ฉด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’จ ์™ธ๋ถ€์˜์กด์„ฑ์ด ์žˆ๋Š” ๊ฐ์ฒด๋Š” Mockito ๋“ฑ์„ ์ด์šฉํ•ด์„œ ์กฐ์ž‘(Mocking)ํ•ฉ๋‹ˆ๋‹ค.

- ๋ฌผ๋ก  ์ด๋ฅผ ์กฐ์ž‘ํ•˜๋ ค๋ฉด, ์™ธ๋ถ€์—์„œ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์˜์กด์„ฑ์„ ์ƒ์„ฑ์ž๋กœ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์–ด์•ผํ•ฉ๋‹ˆ๋‹ค.
- ํŠน์ • ๋ฉ”์„œ๋“œ ex) console.todayAsString() ๊ฐ€ ์‹คํ–‰๋˜๋ฉด, ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
- ํŠน์ • ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋ฉด exception์ด ํ„ฐ์ง€๋„๋ก ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

@Mock Console console;
var myConsolePrinter = new MyConsolePrinter(console);

given(console.todayAsString()) // ์ƒํƒœ๋ฅผ ์กฐ์ž‘, ์ด๋ฅผ Mocking ์ด๋ผ ํ•ฉ๋‹ˆ๋‹ค. 
       .willReturn("01/04/2014", "02/04/2014", "10/04/2014");

var myService = new MyService(myConsolePrinter )


๐Ÿ’จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ž…์žฅ์˜ ์š”๊ตฌ์‚ฌํ•ญ ํ™•์ธ์„, ์ธ์ˆ˜ ํ…Œ์ŠคํŠธ(Acceptance Test)๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

- ์ด๋Š” main ํ•จ์ˆ˜๋‚˜, ์‹ค์ œ ์•ฑ์„ ์‹คํ–‰์‹œ์ผœ์„œ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฒฝ์šฐ๋„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์ž์ฃผํ•ด๋ดค์ฃ . 
- ๋ฌผ๋ก  Acceptance Test ๋„  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์•„๋ž˜์™€ ๊ฐ™์€ mainํ•จ์ˆ˜๊ฐ€ ์žˆ์„ ๋•Œ

public class BankApplication {
    public static void main(String[] args) {
        Clock clock = new Clock();
        TransactionRepository transactionRepository = new TransactionRepository(clock);
        Console console = new Console();
        StatementPrinter statementPrinter = new StatementPrinter(console);
        Account account = new Account(transactionRepository, statementPrinter);

        // ๋‚ด๊ฐ€ ํ™•์ธํ•˜๊ณ  ์‹ถ์€ ๊ฒฐ๊ณผ๋“ค, ์—ฌ๊ธฐ์„  ์‹ค์ œ DB๋‚˜ ์ฝ˜์†”์„ ๋’ค์ ธ๊ฐ€๋ฉฐ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
        account.deposit(1000);
        account.withdraw(100);
        account.deposit(500);
        account.printStatement();
    }
}


- ์ด๋ฅผ ATDD (์ธ์ˆ˜ TDD, AcceptanceTDD) ๋กœ ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ๊ธˆ ์„ค๋ช…ํ•œ Mocking ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•œ๊ฑฐ์ฃ .

@ExtendWith(MockitoExtension.class) // @Mock ์‚ฌ์šฉ์„ ์œ„ํ•œ ์„ค์ •
public class PrintStatementFeature {
    @Mock Console console;
    @Mock Clock clock;
    private Account account;

    @BeforeEach
    void setUp() {
        TransactionRepository transactionRepository = new TransactionRepository(clock);
        StatementPrinter statementPrinter = new StatementPrinter(console);
        account = new Account(transactionRepository, statementPrinter);
    }

    @Test
    void print_statement_containing_all_transactions() {
        given(clock.todayAsString()).willReturn("01/04/2014", "02/04/2014", "10/04/2014");

        account.deposit(1000);
        account.withdraw(100);
        account.deposit(500);
        account.printStatement();

        // ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘” ๊ฐ€์งœ๊ฐ์ฒด(Mock)์„ ์ด์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.
        InOrder inOrder = inOrder(console);
        inOrder.verify(console).printLine("DATE | AMOUNT | BALANCE");
        inOrder.verify(console).printLine("10/04/2014 | 500.00 | 1400.00");
        inOrder.verify(console).printLine("02/04/2014 | -100.00 | 900.00");
        inOrder.verify(console).printLine("01/04/2014 | 1000.00 | 1000.00");
    }
}

 

 

๐Ÿ’จ ๋‹ค๋งŒ ์กฐ์ž‘ํ•˜๊ธฐ ์–ด๋ ค์šด ์™ธ๋ถ€ ์˜์กด์„ฑ(์ฝ˜์†”, DB๋“ฑ)์ด ์—†๋‹ค๋ฉด, ๊ตณ์ด Mockito๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„๋ฉ๋‹ˆ๋‹ค. 

๊ทธ๋ƒฅ ์ง์ ‘ SpyRepository RepositoryStub์ด๋Ÿฐ ์ด๋ฆ„์˜ ๊ฐ€์งœ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๋ฉด ๋˜๊ฑฐ๋“ ์š” 


๐Ÿ’จ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ์•ฑ, ์Šคํ”„๋ง์—์„œ๋„ ๋‹ค ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์™ธ๋ถ€ API, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋ณต์žกํ•œ ์˜์กด์„ฑ.. ๋‹ค ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
ํ…Œ์ŠคํŠธ์˜ ๋Œ€์ƒ์ด (์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์‚ฌ์šฉํ•˜๋Š”) ๋‚ด๊ฐ€ ๋งŒ๋“  ๊ฐ์ฒด์—ฌ์•ผ ์˜๋ฏธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์™ธ๋ถ€์˜์กด์„ฑ ์ž์ฒด๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š”๊ฑด ์•„๋ฌด๋Ÿฐ ์˜๋ฏธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
KakaoPay API ๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€, MySQL DB ์„œ๋ฒ„๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ์Šคํ”„๋ง์—์„œ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฑด ์•„๋ฌด๋Ÿฐ ์˜๋ฏธ๊ฐ€ ์—†์ฃ .

๐Ÿ’จ ์˜ˆ๋ฅผ ๋“ค์–ด @SpringBootTest(webEnviroment=...) ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ต์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

MOCK (์ƒ๋žต์‹œ ๊ธฐ๋ณธ๊ฐ’) = MockMvc ์‚ฌ์šฉ
RANDOM_PORT, DEFINED_PORT = ํฌํŠธ๋ฅผ ์ง€์ •ํ•ด์„œ ์‹ค์ œ ํ†ฐ์บฃ์„ ๋„์›Œ๋ด„

// @SpringBootTest(webEnviroment=...)
= MOCK (์ƒ๋žต์‹œ ๊ธฐ๋ณธ๊ฐ’) = MockMvc ์‚ฌ์šฉ
= RANDOM_PORT, DEFINED_PORT = ํฌํŠธ๋ฅผ ์ง€์ •ํ•ด์„œ ์‹ค์ œ ํ†ฐ์บฃ์„ ๋„์›Œ๋ด„
Mock์—์„œ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด MockMvc๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•˜๊ณ 
Random์„ ์‚ฌ์šฉํ•ด์„œ ์‹ค์ œ ์ปจํ…Œ์ด๋„ˆ์— ์ƒํ˜ธ์ž‘์šฉํ•ด๋ณด๋ ค๋ฉด RestTemplate ๋‚˜ WebClient๋ฅผ ์จ์•ผํ•˜๋Š”๋ฐ, ํ…Œ์ŠคํŠธ ์šฉ๋„๋กœ TestRestTemaplte WebTestClient ๊ฐ™์€๊ฑฐ๋„ ์ œ๊ณตํ•ด์ค๋‹ˆ๋‹ค.
1.MockMvc ์™€ 2. TestRestTemplate์˜ ์ฐจ์ด์ ์€ 1๋ฒˆ์€ ๋‹จ์ˆœํžˆ ๋””์ŠคํŒจ์ณ ์„œ๋ธ”๋ฆฟ์ด ์ž˜ ๋™์ž‘ํ•˜๋Š”๊ฐ€ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๋Š”๊ฑฐ๊ณ , 2๋ฒˆ์€ ์‹ค์ œ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ HTTP ์š”์ฒญ์„ ์ž˜ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, ์„œ๋ธ”๋ฆฟ์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๊ฒฐ๊ตญ ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋งˆ์ง€๋ง‰์— ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋˜๋Š”๋ฐ
  • TestRestTemplate์€ ์‹ค์ œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ HTTP ํ†ต์‹ ์œผ๋กœ ์˜ˆ์™ธํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๊ณ 
  • MockMvc๋Š” ๋‹จ์ˆœํžˆ ๋””์ŠคํŒจ์ณ ์„œ๋ธ”๋ฆฟ์ด ์˜ˆ์™ธ๋ฅผ ์ž˜ throwํ•˜๊ณ  ์ „๋‹ฌํ–ˆ๋Š”์ง€, ๋ฐ˜ํ™˜๊ฐ’์„ ์ž˜ ์ „๋‹ฌํ–ˆ๋Š”์ง€ ๋กœ์ง๋งŒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
+ ๋งŒ์•ฝ ์„œ๋ฒ„์ž…์žฅ์ด ์•„๋‹ˆ๋ผ, ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ ํ…Œ์ŠคํŠธ(ex ์™ธ๋ถ€ API ํ˜ธ์ถœ์ฝ”๋“œ) ํ•˜๊ณ ์‹ถ๋‹ค๋ฉด @RestClientTest ๋ฅผ ์ด์šฉํ•ด MockRestServiceServer๋ฅผ ๋งŒ๋“ค์–ด ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

 

์‹ค์ œ๋กœ ์ž˜ ๋Œ์•„๊ฐ€๋Š”์ง€ ํ†ตํ•ฉํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๋ ค๋ฉด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„์šฐ๋Š”๊ฒŒ ๋งž๊ฒ ์ง€๋งŒ ๋Š๋ฆฌ๊ณ  ๋น„์šฉ์ด ๋งŽ์ด ๋“ค์ฃ .
์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€, ๋กœ์ง์˜ ์œ ํšจ์„ฑ์„ ํŒŒ์•…ํ•˜๋Š”๋ฐ๋Š” ๊ตณ์ด ์‹ค์ œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„์šธ ํ•„์š”๊ฐ€ ์—†์œผ๋‹ˆ๊นŒ MockMvc๋กœ ํ™•์ธํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๊ทผ๋ฐ ์‚ฌ์‹ค ๊ทธ๋Ÿฐ ๊ฒฝ์šฐ๋ผ๋ฉด ๋ชจ๋“  ๋นˆ์„ ์ปจํ…Œ์ด๋„ˆ(AppContext)์— ๋„์šธ ํ•„์š”๋„ ์—†์œผ๋‹ˆ๊นŒ,
SpringBootTest ๋ง๊ณ  ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ๋นˆ๋งŒ ๋„์šธ ์ˆ˜ ์žˆ๋Š” @WebMvcTest ๊ฐ™์€ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์“ฐ๋Š”๊ฒŒ ์ œ์ผ ํ…Œ์ŠคํŠธ ์†๋„๊ฐ€ ๋นจ๋ผ์„œ ์ข‹์ฃ .

 

# MockMvc ์‚ฌ์šฉ๋ฒ•

์š”์ฒญ ๋งŒ๋“ค๊ธฐ

// ๐Ÿ’จ MockMvcRequestBuilders๋Š” RequestBuilder์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„ ๊ฐ์ฒด์ด๋‹ค. 
// RequestBuilder reqBuilder = MockMvcRequestBuilders.get("/");
MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/");

request.param("name", "์ด๋ฆ„")
       .content("content")
       .sessionAttr("session", "์„ธ์…˜");

 

์‹คํ–‰ ๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ResultActions๋กœ ๋ฐ›์•„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

RequestBuilder request = MockMvcRequestBuilders.get("/admin/login");
var actions = mockMvc.perform(request); // ์‹คํ–‰๊ฒฐ๊ณผ ๋ฐ›๊ธฐ, ResultActions ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜

actions.andDo(print())
    .andExpect(status().isOk())
    .andExpect(model().attributeExists("signUpForm"))
    .andExpect(view().name("account/sign-up"));

ResultActions ๊ฐ์ฒด์— andExpect() ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด ๋ฉ”์„œ๋“œ๋Š” ์ธ์ž ๊ฐ’์œผ๋กœ org.springframework.test.web.servlet.ResultMatcher ๋ฅผ ๋ฐ›๋Š”๋‹ค

spring-boot-test์—์„œ ์ œ๊ณตํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ์ธ MockMvcResultMatchers ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ•˜๋ฉด ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

์ฐธ๊ณ ๋กœ ResultActions์—๋Š” ๊ฒ€์ฆ๋ง๊ณ  ํŠน์ • ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค๊ฑฐ๋‚˜, ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ํ™”๋ฉด์— ์ฐ๋Š”๋‹ค๊ฑฐ๋‚˜

ResultActions ์˜ ๋ฉ”์†Œ๋“œ 

public static ResultHandler log()

: ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๋””๋ฒ„๊น… ๋ ˆ๋ฒจ์—์„œ ๋กœ๊ทธ๋กœ ์ถœ๋ ฅํ•œ๋‹ค. ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๋กœ๊ฑฐ ์ด๋ฆ„์€  org.springframework.test.web.servlet.result๋‹ค

public static ResultHandler print()

: ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ์ž„์˜์˜ ์ถœ๋ ฅ ๋Œ€์ƒ์— ์ถœ๋ ฅํ•œ๋‹ค. ์ถœ๋ ฅ๋Œ€์ƒ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ํ‘œ์ค€ ์ถœ๋ ฅ(System.out)์ด ์ถœ๋ ฅ ๋Œ€์ƒ์ด ๋œ๋‹ค

 

 

์š”์ฒญ๋งŒ๋“ค๊ธฐ - MockHttpServletRequestBuilder

public MockHttpServletRequestBuilder param(String name, String... values)

public MockHttpServletRequestBuilder params(MultiValueMap<String,String> params)

: ์š”์ฒญํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์„ค์ •ํ•œ๋‹ค

public MockHttpServletRequestBuilder header(String name, Object... values)

public MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)

: ์š”์ฒญ ํ—ค๋”๋ฅผ ์„ค์ •ํ•œ๋‹ค. contentType์ด๋‚˜ accept์™€ ๊ฐ™์€ ํŠน์ • ํ—ค๋”๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋„ ์ œ๊ณตํ•œ๋‹ค.

public MockHttpServletRequestBuilder cookie(Cookie... cookies)

: ์ฟ ํ‚ค๋ฅผ ์„ค์ •ํ•œ๋‹ค

public MockHttpServletRequestBuilder content(String content)

: ์š”์ฒญ ๋ณธ๋ฌธ์„ ์„ค์ •ํ•œ๋‹ค

public MockHttpServletRequestBuilder requestAttr(String name, Object value)

: ์š”์ฒญ์Šค์ฝ”ํ”„์— ๊ฐ์ฒด๋ฅผ ์„ค์ •ํ•œ๋‹ค

public MockHttpServletRequestBuilder sessionAttr(String name, Object value)

public MockHttpServletRequestBuilder sessionAttrs(Map<String,Object> sessionAttributes)

: ์„ธ์…˜ ์Šค์ฝ”ํ”„์— ๊ฐ์ฒด๋ฅผ ์„ค์ •ํ•œ๋‹ค.

public MockHttpServletRequestBuilder flashAttr(String name, Object value)

: ํ”Œ๋ž˜์‰ฌ ์Šค์ฝ”ํ”„์— ๊ฐ์ฒด๋ฅผ ์„ค์ •ํ•œ๋‹ค.

 

๊ฒ€์ฆ - MockMvcResultMatchers

public static ViewResultMatchers view()

: ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๋ทฐ ์ด๋ฆ„์„ ๊ฒ€์ฆํ•œ๋‹ค.

public static StatusResultMatchers status()

: HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

public static HeaderResultMatchers header()

: ์‘๋‹ต ํ—ค๋”์˜ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

public static ContentResultMatchers content()

: ์‘๋‹ต ๋ณธ๋ฌธ ๋‚ด์šฉ์„ ๊ฒ€์ฆํ•œ๋‹ค. jsonPath๋‚˜ xpath์™€ ๊ฐ™์€ ํŠน์ • ์ฝ˜ํ…์ธ ๋ฅผ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค

public static CookieResultMatchers cookie()

: ์ฟ ํ‚ค์˜ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค

public static ModelResultMatchers model()

: ์Šคํ”„๋ง MVC ๋ชจ๋ธ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

public static ResultMatcher redirectedUrl(String expectedUrl)

: ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋Œ€์ƒ์˜ ๊ฒฝ๋กœ ๋˜๋Š” URL์„ ๊ฒ€์ฆํ•œ๋‹ค. ํŒจํ„ด์œผ๋กœ ๊ฒ€์ฆํ•  ๋•Œ๋Š” redirectedUrlPattern ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

public static ResultMatcher forwardedUrlPattern(String urlPattern)

: ์ด๋™ ๋Œ€์ƒ ๊ฒฝ๋กœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. ํŒจํ„ด์œผ๋กœ ๊ฒ€์ฆํ•  ๋•Œ๋Š” forwardedUrlPattern ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

public static RequestResultMatchers request()

: ์„œ๋ธ”๋ฆฟ 3.๋ถ€ํ„ฐ ์ง€์›๋˜๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ์ƒํƒœ๋‚˜ ์š”์ฒญ์Šค์ฝ”ํ”„์˜ ์ƒํƒœ, ์„ธ์…˜ ์Šค์ฝ”ํ”„์˜ ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

 


 

# ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ๊ฐ€ ์™œ ํ•„์š”ํ•˜์ฃ ?

์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ๋Š” ์ „์ฒด์ค‘ ์ผ๋ถ€๋งŒ ๊ฐ€์ ธ์™€์„œ ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

  • @SpringBootTest๋Š” ํ…Œ์ŠคํŠธ์™€ ์ƒ๊ด€์—†๋Š” ์•ฑ ์„ค์ •, ๋ชจ๋“  Bean๋“ค์„ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ์— ์˜ค๋ž˜๊ฑธ๋ฆฐ๋‹ค.
    โžก @SpringBootTest๋Š” ๋‹จ์œ„ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ•˜๊ธฐ์— ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค.
  • ํ…Œ์ŠคํŠธ์˜ ๋‹จ์œ„๊ฐ€ ์ปค์ ธ ๋””๋ฒ„๊น…์ด ์–ด๋ ค์›Œ์ง„๋‹ค.
  • ๊ทธ๋ ‡๋‹ค๊ณ  @Test๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ์—๋Š” ์Šคํ”„๋ง ํ…Œ์ŠคํŠธ ์ค€๋น„ ์ž‘์—…์ด ๋„ˆ๋ฌด ๋ฒˆ๊ฑฐ๋กญ๋‹ค.

 

# SpringBoot์˜ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ

์–ด๋…ธํ…Œ์ด์…˜์„ ํ…Œ์ŠคํŠธ Class์— ๋ถ™์—ฌ์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ์ œ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

 

@WebMvcTest - MockApiTest (MVC ๊ด€๋ จ๋œ Bean)

โžก MockMvc์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑ/์„ค์ •ํ•ด์ฃผ์–ด MVC์™€ ๊ด€๋ จ๋œ ๋‹จ์œ„ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.

โžก @Mock ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์Šคํ”„๋ง ๋นˆ์„ ์œ„ํ•œ @MockBean ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ณตํ•ด์ค€๋‹ค. (๊ป๋ฐ๊ธฐ๋งŒ ์žˆ์Œ. ๊ตฌํ˜„ X)

@WebMvcTest(PaymentController.class)
public class PaymentControllerTests {
    @AutoWired 
    private MockMvc mvc;
    
    @MockBean
    private PaymentService paymentService; 
    // PaymentService ๋‚ด๋ถ€์—์„œ ์™ธ๋ถ€์˜ ๊ฒฐ์ œ ๋Œ€ํ–‰ ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ๋ผ๊ณ  ๊ฐ€์ •
  	
    @Test
    public void testPayment() throws Exception {
        // 5๋งŒ์› ๊ธˆ์•ก ์ถฉ์ „: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ์‹คํŒจํ•˜๋Š” ํ–‰์œ„์ด์ง€๋งŒ
        given(this.payMentService.chargePoint(50000L)) 
          .willReturn(new Point(50000L)); // ์˜ฌ๋ฐ”๋ฅธ ์š”์ฒญ์œผ๋กœ ๊ฐ„์ฃผํ•˜๊ณ  ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ–‰์œ„ ์ง€์ •
    }
}
// ํ…Œ์ŠคํŠธํ•  ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค๋ฅผ ์ง€์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
@WebMvcTest(ArticleApiController.class)
public class ArticleApiControllerTest {

  @Autowired
  private MockMvc mockMvc; // MockMvc๋ฅผ ์ž๋™์œผ๋กœ ์„ค์ •ํ•ด์ฃผ๋ฉฐ, ์ด๋ฅผ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.
  
  @MockBean // ๋ฌผ๋ก  MockBean์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ์ง์ ‘ ๊ฐ€์งœ๊ฐ์ฒด(Stub)์„ ๋งŒ๋“ค์–ด๋„ ๋œ๋‹ค.
  private ArticleService articleService;

  @DisplayName("ํ† ํฐ๊ณผ ์ด๋ฉ”์ผ ์ •๋ณด๋กœ ๊ฒ€์ฆ์š”์ฒญ์„ ๋ณด๋‚ธ ๊ฒฝ์šฐ 303 HTTP Code ๋ฆฌํ„ดํ•œ๋‹ค.")
  void valid_success() throws Exception {
     //Given
     var request = new LoginAccountRequest();
     request.setEmail(args);
     request.setPassword(args);

     //When. ์ฐธ๊ณ ๋กœ ๊ตณ์ด actions ๋ณ€์ˆ˜๋กœ ๋ฐ›์ง€ ์•Š์•„๋„ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๊ธด ํ•˜๋‹ค.
     var actions = mockMvc.perform(post("/api/login")
         .accept(MediaType.APPLICATION_JSON) // accept ํ—ค๋” ์„ค์ •
         .content(objectMapper.writeValueAsString(request)) // content Body ์„ค์ •
         .param("email","hi@naver.com") // ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •
      	
      );

      //Then
      actions
          .andDo(print()) // ํ…Œ์ŠคํŠธ์— ์„ฑ๊ณตํ•˜๋”๋ผ๋„ ์ฝ˜์†”์— Printํ•˜๋„๋ก ํ•œ๋‹ค.
          .andExpect(status().isOk())  //200์ฝ”๋“œ
          .andExpect(jsonPath("$.id").isNumber()) // Json๋„ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ™•์ธ๊ฐ€๋Šฅ
          .andExpect(jsonPath("$.email").isString())
          .andExpect(jsonPath("$.nickname").isString());
  }
}

 

 

์ฐธ๊ณ ๋กœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋น„๋™๊ธฐ๋กœ ์‘๋‹ต (Future๋‚˜ DeferredResult๋ฅผ ๋ฐ˜ํ™˜)ํ•˜๋Š” ๊ฒฝ์šฐ ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

    // MvcResult ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋น„๋™๊ธฐ ์‘๋‹ต์„ ๋ฐ›์Œ (Future, DeferredResult)    
    MvcResult result = mockMvc.perform(get("/api/articles/1")).andReturn();
    
    Mockmvc.perform(asyncDispatch(result)) // asyncDispatch๋ฅผ ์ด์šฉ.
            .andExpect(status().isOk())
            .andExpect(jsonPath("@.id").value(1));

 


@DataJpaTest - RepositoryTest (Jpa ๊ด€๋ จ  Bean)

โžก ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋ฅผ ์ƒ์„ฑํ•˜๊ณ  @Transaction์„ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์ค€๋‹ค. JPA์™€ ๊ด€๋ จ๋œ ๋‹จ์œ„ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.

์ฐธ๊ณ ๋กœ Spring Data JPA๋ง๊ณ ๋„ @JdbcTest, @DataMongoTest ๋“ฑ์˜ ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ๋„ ์žˆ๋‹ค.

// ํ”„๋กœ์ ํŠธ ๋‚ด์˜ @Entity๋ฅผ ์Šค์บ”ํ•˜๊ณ , TestEntityManager ๋นˆ์„ ์ž๋™ ์„ค์ •ํ•ด์ค€๋‹ค.
@DataJpaTest
public class ArticleDaoTest {

    @Autowired
    private TestEntityManager entityManager;
    @Autowired
    private ArticleDao articleDao;

    @Test
    public void test() {
        Article articleByKwseo = new Article(1, "kwseo", "good", "hello", Timestamp.valueOf(
            LocalDateTime.now()));
        Article articleByKim = new Article(2, "kim", "good", "hello",
            Timestamp.valueOf(LocalDateTime.now()));
        
        entityManager.persist(articleByKwseo);
        entityManager.persist(articleByKim);

        List<Article> articles = articleDao.findByAuthor("kwseo");
        assertThat(articles)
            .isNotEmpty()
            .hasSize(1)
            .contains(articleByKwseo)
            .doesNotContain(articleByKim);
    }
}

 

์ฐธ๊ณ ๋กœ @DataJpaTest ์•ˆ์—๋Š” @Transactional ํ‚ค์›Œ๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค. ๋งŒ์•ฝ ์˜๋„์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์ ์šฉํ•˜๊ณ  ์‹ถ์ง€ ์•Š๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด propagation์„ ๋ฐ”๊ฟ”์ฃผ์ž.

// @Entity๋ฅผ ์Šค์บ”ํ•˜๊ณ  TestEntityManager ๋นˆ์„ ์ƒ์„ฑํ•œ๋‹ค.
@DataJpaTest 
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class SomejpaTest {
    ...
}

 


@JsonTest

โžก JacksonTester๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋ฉฐ JSON์˜ ์ง๋ ฌํ™”, ์—ญ์ง๋ ฌํ™”ํ•ด๋ณด๊ณ  ํŒŒ์ผ๊ณผ ๋น„๊ตํ•˜๋Š” ๋“ฑ์˜ ํ…Œ์ŠคํŠธ ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

@JsonTest
public class ArticleJsonTest {

    @Autowired
    private JacksonTester<Article> json;

    @Test
    public void testSerialize() throws IOException {
        Article article = new Article(
            1,
            "kwseo",
            "good",
            "good article",
            Timestamp.valueOf((LocalDateTime.now())));

        // assertThat(json.write(article)).isEqualToJson("expected.json");  ์ง์ ‘ ํŒŒ์ผ๊ณผ ๋น„๊ต
        assertThat(json.write(article)).hasJsonPathStringValue("@.author");
        assertThat(json.write(article))
            .extractingJsonPathStringValue("@.title")
            .isEqualTo("good");
    }
}

 


@RestClientTest

โžก ์š”์ฒญ์— ๋ฐ˜์‘ํ•˜๋Š” MockRestServiceServer ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค๋ฉฐ, ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ ์ ‘์†ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธํ•œ๋‹ค.

@RestClientTest(ArticleServiceImpl.class)
public class ArticleServiceImplWithRestClientTest {

    @MockBean
    private ArticleDao dao;
    @Autowired
    private ArticleServiceImpl service;
    @Autowired
    private MockRestServiceServer server;

    @Test
    public void testGetFindOneFromRemote() throws Exception {
        String articleJson = ...;
		
        // ์„œ๋ฒ„์˜ Response๋ฅผ ์Šคํ„ฐ๋น™ํ•จ. 
        server.expect(requestTo("http://sample.com/some/articles/1"))
            .andRespond(withSuccess(articleJson, MediaType.APPLICATION_JSON));
		
        // ๊ฒŒ์‹œํŒ article์„ ์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜ด
        Article article = service.findOneFromRemote(1);
        assertThat(article.getId()).isEqualTo(1);
    }
}

 


@WebFluxTest

โžก ์Šคํ”„๋ง WebFlux ์™€ ๊ด€๋ จ๋œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ž๋™ ๊ตฌ์ถ•ํ•˜๊ณ , ๋‹จ์œ„ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.

์ฐธ๊ณ ๋กœ @WebFluxTest๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์„ค์ •์ด ํฌํ•จ๋˜์–ด ์žˆ๋‹ค.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(WebFluxTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(WebFluxTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureJson
@AutoConfigureWebFlux
@AutoConfigureWebTestClient
@ImportAutoConfiguration
public @interface WebFluxTest {
}

 

์ฐธ๊ณ ๋กœ WebFlux๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋”๋ผ๋„ ํ•ด๋‹น ํ…Œ์ŠคํŠธ๋ฅผ ์ด์šฉํ•˜๋ฉด BDD (Given-When-Then) ํ…Œ์ŠคํŠธ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ์ž์„ธํ•œ๊ฑด ์•„๋ž˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

 

Spring WebFlux์™€ Kotlin์œผ๋กœ ๋งŒ๋“œ๋Š” Todo ์„œ๋น„์Šค – ํ…Œ์ŠคํŠธ ์Šฌ๋ผ์ด์Šค ์ ์šฉํ•˜๊ธฐ | Popit

Spring WebFlux์™€ Kotlin์œผ๋กœ ๋งŒ๋“œ๋Š” Todo ์„œ๋น„์Šค – 1ํŽธ Spring WebFlux์™€ Kotlin์œผ๋กœ ๋งŒ๋“œ๋Š” Todo ์„œ๋น„์Šค – 2ํŽธ Spring WebFlux์™€ Kotlin์œผ๋กœ ๋งŒ๋“œ๋Š” Todo ์„œ๋น„์Šค – ํ…Œ์ŠคํŠธ ์Šฌ๋ผ์ด์Šค ์ ์šฉํ•˜๊ธฐ ๊ฐœ์š” ์ง€๋‚œ ์˜ˆ์ œ๋“ค์—์„ 

www.popit.kr

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ