๐Ÿ’ Spring

Mockito Framework์— ๋Œ€ํ•ด์„œ

iseunghan 2022. 5. 1. 19:36
๋ฐ˜์‘ํ˜•

Mockito๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž

ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ: Spring-boot, Junit5, Mockito
๋ชจ๋“  ์ฝ”๋“œ๋Š” Github์— ์žˆ์Šต๋‹ˆ๋‹ค :)

Mockito

Mockito๋ž€? ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ํ…Œ์ŠคํŠธํ•  ๋•Œ ๋ชจ๋“  ๋นˆ์„ ์ผ์ผํžˆ ์ฃผ์ž…์‹œํ‚ค์ง€ ์•Š๊ณ , Mock ๊ฐ์ฒด(๊ฐ€์งœ ๊ฐ์ฒด)๋ฅผ ์ฃผ์ž…์‹œ์ผœ ํ–‰์œ„๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ณ , ์•„์ง ์ž‘์„ฑ๋˜์ง€ ์•Š์€ ์ฝ”๋“œ๋“ค(์˜์กด์„ฑ ๊ฐ์ฒด) ๋˜๋Š” ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์šด ์˜์กด์„ฑ ๊ฐ์ฒด๋“ค์„ Mockingํ•˜์—ฌ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜์กด์„ฑ (dependency)

spring boot๋ฅผ ์‚ฌ์šฉ์ค‘์ด๋ผ๋ฉด, spring-boot-starter-test ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ด๋ฏธ JUnit5์™€ Mockito๊ฐ€ ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฏ€๋กœ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Spring.io - 41.Testing

Mock ํ…Œ์ŠคํŠธ ์ˆœ์„œ

  1. mock ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐ ์ฃผ์ž…
  2. Stubbing : Mock์˜ ํ–‰๋™์„ ๊ตฌ์ฒด์ ์œผ๋กœ ๋ช…์„ธํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์ „์— ์ •์˜๋œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  3. Verify : Mock์˜ ํ–‰๋™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ์šฉ BookService

@Service
public class BookService {
    @Autowired
    private BookRepository bookRepository;
    public Book findAll() {
        List<Book> bookList = bookRepository.findAll();
        if(bookList.size() == 0) {
            throw new BookEmptyException();
        }
        return bookList;
    }
    public Book findById(Long id) {
        Book book = bookRepository.findById(id);
        if(book == null) {
            throw new BookNotFoundException();
        }
        return book;
    }
    ..
}

์ •๋ง ๊ฐ„๋‹จํ•œ ๋ชจ๋“  Book์„ ์กฐํšŒํ•˜๊ณ , ํ•˜๋‚˜์˜ book์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ์ด ๊ตฌํ˜„๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค.

Mockito๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์„ค์ •

JUnit 5๋Š” MockitoExtension์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

@Extendwith(MockitoExtension.class)
public class BookServiceTest {
}

Mock ๊ฐ์ฒด ์ƒ์„ฑ

Mock ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ•„๋“œ์— ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ๊ฐ์ฒด๋“ค์„ Mock์œผ๋กœ ์ฃผ์ž…์‹œ์ผœ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋กœ @Mock, @InjectMocks์ž…๋‹ˆ๋‹ค.

@Extendwith(MockitoExtension.class)
public class BookServiceTest {
    // ํ•ด๋‹น ๊ฐ์ฒด๋ฅผ Mock ์ฒ˜๋ฆฌํ•œ๋‹ค. (mock(BookRepository.class)์™€ ๊ฐ™์€ ํ‘œํ˜„์ž…๋‹ˆ๋‹ค.)
    @Mock
    private BookRepository bookRepository;
    // ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๊ฐ์ฒด์— @Mock์ด ๋ถ™์€ ๊ฐ์ฒด๋“ค์„ ์ฃผ์ž…์‹œ์ผœ์ค๋‹ˆ๋‹ค. (ํ•„๋“œ์— ์žˆ๋Š” ๊ฐ์ฒด๋“ค๊ณผ ๋™์ผํ•˜๋‹ค๋ฉด)
    @InjectMocks
    private BookService bookService;
}

@Spy ์™€ @Mock ์˜ ์ฐจ์ด?

@Mock๊ฐ์ฒด๋Š” ๊ฐ€์งœ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ํ…Œ์ŠคํŠธํ•˜๋ ค๋Š” ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ Stub์„ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ Stub์„ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด Mockito ๊ธฐ๋ณธ์ „๋žต์ธ Answers.RETURNS_DEFAULT์— ์˜ํ•ด ๋ฆฌํ„ดํƒ€์ž…์— ๋งž๋Š” ๊ธฐ๋ณธ ๋ฉ”์†Œ๋“œ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Mockito - mockito-core 4.5.1 javadoc

๋ฐ˜๋ฉด์— @Spy๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ์ฃผ์ž…ํ•ด์ค๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ๋”ฐ๋กœ Stub์„ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ๊ธฐ์กด์— ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Stubbing

BookService์˜ findAll() ๋ฉ”์†Œ๋“œ์˜ ํ–‰๋™์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ด Stubbingํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

findAll() ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€์— ๋ณด๋ฉด bookRepository.findAll() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ํ•ด๋‹น ๋ฉ”์†Œ๋“œ์˜ ํ–‰๋™์„ ์ •์˜ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

bookRepository์˜ ์–ด๋–ค ๋ฉ”์†Œ๋“œ๋ฅผ ์–ด๋–ค ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ํ˜ธ์ถœ ํ–ˆ๊ณ , ์–ด๋–ค ๊ฐ’์„ ๋ฆฌํ„ดํ•ด์ฃผ๋Š”์ง€๋ฅผ ์ •์˜ํ•ด์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

findAll() stubbing

List<Book> bookList = new ArrayList<>();
bookList.add(new Book(1L, "title"));
bookList.add(new Book(2L, "title2"));
// stubbing
when(bookRepository.findAll()).thenReturn(bookList);

findById() stubbing

Book book = new Book(1L, "title");
// stubbing
when(bookRepository.findById(anyLong()).thenReturn(book);

Verify

ํšŸ์ˆ˜์— ๋Œ€ํ•ด ๊ฒ€์ฆ

// ํ˜ธ์ถœ๋˜์ง€ ์•Š์Œ
verify(bookRepository, never()).findAll();
verify(bookRepository, times(0)).findAll();    // never()๊ณผ ๋™์ผ
// 1๋ฒˆ ํ˜ธ์ถœ ๋จ
verify(bookRepository).findAll();        // times(1)๊ณผ ๋™์ผ
// ์ ์–ด๋„ 1๋ฒˆ์ด์ƒ ํ˜ธ์ถœ ๋จ
verify(bookRepository, atLeastOnce()).findAll();
verify(bookRepository, atLeast(1)).findAll();    // ๋™์ผ
// ์ตœ๋Œ€ N๋ฒˆ์ดํ•˜ ํ˜ธ์ถœ ๋จ
verify(bookRepository, atMost(N)).findAll();

์ˆœ์„œ์— ๋Œ€ํ•ด ๊ฒ€์ฆ

์˜ˆ๋ฅผ ๋“ค์–ด, Book์„ ์ถ”๊ฐ€ํ•  ๋•Œ BookRepository์— ํ•ด๋‹น id๋กœ ์ด๋ฏธ ์ถ”๊ฐ€๋œ ์ฑ…์ด ์žˆ๋Š”์ง€ 1.์กฐํšŒ ํ•ด๋ณด๊ณ  ์ค‘๋ณต๋˜์ง€ ์•Š์•˜์œผ๋ฉด 2.์ถ”๊ฐ€๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.
ํ•ด๋‹น ์ˆœ์„œ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค๊ณ  ํ–ˆ์„ ๋•Œ ๊ฒ€์ฆ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// ํŽธ์˜์ƒ ๋ฉ”์†Œ๋“œ ์ด๋ฆ„์€ ์กฐํšŒ, ์ถ”๊ฐ€๋กœ ์„ค์ •
InOrder inOrder = inOrder(bookRepository);
inOrder.verify(bookRepository).์กฐํšŒ(anyLong());
inOrder.verify(bookRepository).์ถ”๊ฐ€(any(Book.class));

Exception ๊ฒ€์ฆ

bookService.findById()๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ์š”?
bookRepository.findById()์ด null ์„ ๋ฆฌํ„ดํ•˜๊ฒŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ , findById๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์ž˜ ๋˜์ง€๋Š”์ง€ ํ™•์ธํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

when(bookRepository.findById(anyLong())).thenReturn(null);
assertThrows(NotFoundException.class, () -> bookService.findById(1L));

Controller ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ BookController

@RestController
public class BookController {

    @Autowired
    private BookService bookService;

    @GetMapping("/books/{book_id}")
    public Book findBook(@PathVariable Long book_id) {
        return bookService.findBook(book_id);
    }

    @GetMapping("/books")
    public List<Book> findAll() {
        return bookService.findBooks();
    }

    @PostMapping("/books")
    public Long addBook(@RequestBody Book book) {
        return bookService.saveBook(book);
    }

    @ExceptionHandler(BookDuplicateException.class)
    public ResponseEntity _400() {
        return ResponseEntity.badRequest().build();
    }

    @ExceptionHandler(BookNotFoundException.class)
    public ResponseEntity _404() {
        return ResponseEntity.notFound().build();
    }
}

BookControllerTest

// ๋ชจ๋“  ๋นˆ๋“ค์„ ์ปจํ…์ŠคํŠธ์— ์˜ฌ๋ฆฌ์ง€ ์•Š๊ณ , MVCํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ Spring MVC components (i.e. @Controller, @ControllerAdvice, @JsonComponent ๋“ฑ)๋งŒ ์˜ฌ๋ผ๊ฐ„๋‹ค.
@WebMvcTest(controllers = BookController.class) // ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ๋งŒ context์— ์˜ฌ๋ ค์ค€๋‹ค.
//@ExtendWith(MockitoExtension.class)
class BookControllerTest {

    // WebMvcTest์—๋Š” @AutoConfigureMockMvc๊ฐ€ ๋ถ™์–ด์žˆ๋‹ค.
    @Autowired
    private MockMvc mockMvc;

    // bookService๋ฅผ mockBean์œผ๋กœ ๋“ฑ๋ก์‹œํ‚จ๋‹ค.
    @MockBean
    private BookService bookService;

    @Autowired
    private ObjectMapper objectMapper;

    private List<Book> bookList;

    @BeforeEach
    void setup() {
        // .. ์ƒ๋žต
    }

    @Test
    void book_์ €์žฅ_200() throws Exception {
        // given
        Book book = Book.builder()
                .id(100L)
                .title("title100")
                .price(1000).build();

        // stubbing
        given(bookService.saveBook(any(Book.class))).willReturn(anyLong());

        // when & then
        mockMvc.perform(post("/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(book)))
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    void ์ค‘๋ณต๋œ_book_์ €์žฅ_400() throws Exception{
        // given
        Book book = bookList.get(0);

        given(bookService.saveBook(any(Book.class))).willThrow(BookDuplicateException.class);

        // when & then
        mockMvc.perform(post("/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(book)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

    @Test
    void ๋ชจ๋“ _book_์กฐํšŒ_200() throws Exception {
        // given
        given(bookService.findBooks()).willReturn(bookList);

        // when & then
        mockMvc.perform(get("/books")
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("[0].id").exists())
                .andExpect(jsonPath("[1].id").exists())
                .andExpect(jsonPath("[2].id").exists());
    }

    @Test
    void ํ•˜๋‚˜์˜_book_์กฐํšŒ_200() throws Exception {
        // given
        Book book = bookList.get(0);

        given(bookService.findBook(anyLong())).willReturn(book);

        // when & then
        mockMvc.perform(get("/books/{id}", anyLong())
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("id").exists())
                .andExpect(jsonPath("title").exists());
    }

    @Test
    void ์—†๋Š”_book_์กฐํšŒ_404() throws Exception {
        // given
        Book book = bookList.get(0);

        given(bookService.findBook(anyLong())).willThrow(BookNotFoundException.class);

        // when & then
        mockMvc.perform(get("/books/{id}", anyLong())
                        .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isNotFound());
    }
}
  • @WebMvcTest๋ฅผ ๋ถ™์—ฌ์ฃผ๊ฒŒ ๋˜๋ฉด, MVC ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์†Œ์˜ ๋นˆ๋“ค์„ ์ปจํ…์ŠคํŠธ์— ๋“ฑ๋กํ•ด์ค๋‹ˆ๋‹ค. (@SpringBootTest๋ณด๋‹ค ๊ฐ€๋ณ์Šต๋‹ˆ๋‹ค.)
    • ์—ฌ๊ธฐ์— controllers ์˜ต์…˜์„ ์ฃผ๊ฒŒ๋˜๋ฉด, ํ•ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด์ œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด๋“ค์„ ๋นˆ์œผ๋กœ ๋“ฑ๋ก์‹œํ‚ฌ ๋•Œ๋Š” @MockBean์„ ์‚ฌ์šฉํ•˜์—ฌ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

REFERENCES

nesoy.github.io
cobbybb.tistory.com
velog.io/@ausg

๋ฐ˜์‘ํ˜•