[6/26 TIL] SPRING(JUnit, Mock, Spring의 JUnit)

yumyeonghanΒ·2023λ…„ 6μ›” 28일
0
post-custom-banner

πŸƒν”„λ‘œκ·Έλž˜λ¨ΈμŠ€ λ°±μ—”λ“œ λ°λΈŒμ½”μŠ€ 4κΈ° κ΅μœ‘κ³Όμ •μ„ λ“£κ³  μ •λ¦¬ν•œ κΈ€μž…λ‹ˆλ‹€.πŸƒ

JUnit

  • 맀 λ‹¨μœ„(λ©”μ„œλ“œ, 클래슀) ν…ŒμŠ€νŠΈμ‹œλ§ˆλ‹€ ν…ŒμŠ€νŠΈ 클래슀의 μΈμŠ€ν„΄μŠ€κ°€ μƒμ„±λ˜μ–΄ 독립적인 ν…ŒμŠ€νŠΈκ°€ κ°€λŠ₯ν•˜κ²Œ 함
  • μ• λ…Έν…Œμ΄μ…˜μ„ μ œκ³΅ν•΄μ„œ ν…ŒμŠ€νŠΈ 라이프 사이클을 κ΄€λ¦¬ν•˜κ²Œ ν•΄μ£Όκ³  ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό κ°„κ²°ν•˜κ²Œ μž‘μ„±ν•˜λ„λ‘ 지원
  • assert둜 ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€μ˜ μˆ˜ν–‰ κ²°κ³Όλ₯Ό νŒλ³„ β†’ assertEquals(μ˜ˆμƒ κ°’, μ‹€μ œ κ°’)
  • κ²°κ³ΌλŠ” 성곡(녹색), μ‹€νŒ¨(뢉은색) 쀑 ν•˜λ‚˜λ‘œ ν‘œμ‹œ

JUnit5 λͺ¨λ“ˆ

  • JUnit Platform
    • JVM 상에 ν…ŒμŠ€νŒ… ν”„λ ˆμž„μ›Œν¬λ₯Ό λŸ°μΉ­ν•˜κΈ° μœ„ν•œ 근간을 제곡
    • TestEngine μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν†΅ν•΄μ„œ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜κ³  ,κ²°κ³Όλ₯Ό 보고
  • JUnit Jupiter
    • JUnit 5에 μƒˆλ‘­κ²Œ μΆ”κ°€λœ ν…ŒμŠ€νŠΈ μ½”λ“œμš© APIλ‘œμ„œ, TestEngine의 μ‹€μ œ κ΅¬ν˜„μ²΄
  • JUnit Vintage
    • 기쑴에 JUnit 4 λ²„μ „μœΌλ‘œ μž‘μ„±ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μ‹€ν–‰ν•  λ•Œ μ‚¬μš©ν•˜λŠ” TestEngine의 μ‹€μ œ κ΅¬ν˜„μ²΄
      좜처: Architectural building blocks of JUnit5

μ˜ˆμ‹œ μ½”λ“œ

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("두 수λ₯Ό 더할 수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldAddTwoNumbers() {
        // given
        int a = 5;
        int b = 3;

        // when
        int result = calculator.add(a, b);

        // then
        assertThat(result).isEqualTo(8);
    }

    @Test
    @DisplayName("두 수λ₯Ό λΊ„ 수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldSubtractTwoNumbers() {
        // given
        int a = 5;
        int b = 3;

        // when
        int result = calculator.subtract(a, b);

        // then
        assertThat(result).isEqualTo(2);
    }

    @Test
    @DisplayName("두 수λ₯Ό κ³±ν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldMultiplyTwoNumbers() {
        // given
        int a = 5;
        int b = 3;

        // when
        int result = calculator.multiply(a, b);

        // then
        assertThat(result).isEqualTo(15);
    }

    @Test
    @DisplayName("두 수λ₯Ό λ‚˜λˆŒ 수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldDivideTwoNumbers() {
        // given
        int a = 10;
        int b = 2;

        // when
        int result = calculator.divide(a, b);

        // then
        assertThat(result).isEqualTo(5);
    }

    @Test
    @DisplayName("0으둜 λ‚˜λˆŒ 경우 μ˜ˆμ™Έλ₯Ό λ˜μ Έμ•Ό ν•œλ‹€.")
    void shouldThrowExceptionWhenDividingByZero() {
        // given
        int a = 10;
        int b = 0;

        // when, then
        assertThatThrownBy(() -> calculator.divide(a, b))
                .isInstanceOf(ArithmeticException.class)
                .hasMessage("Divisor cannot be zero!");
    }
}

@Before(after)Each, @Before(after)All

  • @Before(after)Each
    • 각 ν…ŒμŠ€νŠΈ 클래슀의 λ©”μ„œλ“œλ§ˆλ‹€ μ‹€ν–‰ μ „(ν›„)에 ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μ΄ 뢙은 λ©”μ„œλ“œ μ‹€ν–‰
    • @BeforeEach μ–΄λ…Έν…Œμ΄μ…˜μ΄ μ§€μ •λœ λ©”μ„œλ“œλŠ” static이 μ•„λ‹˜
    • ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλ§ˆλ‹€ λ…λ¦½μ μœΌλ‘œ 섀정이 ν•„μš”ν•œ κ²½μš°μ— μ‚¬μš©
  • @Before(after)All
    • ν…ŒμŠ€νŠΈ 클래슀 μ‹€ν–‰ μ „(ν›„)에 ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μ΄ 뢙은 λ©”μ„œλ“œκ°€ ν•œ 번만 μ‹€ν–‰
    • @BeforeAll μ–΄λ…Έν…Œμ΄μ…˜μ΄ μ§€μ •λœ λ©”μ„œλ“œλŠ” static이어야 함
    • μ—¬λŸ¬ ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμ—μ„œ κ³΅ν†΅μ μœΌλ‘œ μ‚¬μš©λ˜λŠ” λ¦¬μ†ŒμŠ€λ₯Ό μ„€μ •ν•˜λŠ” 데 μ‚¬μš©
    • @TestInstance(TestInstance.Lifecycle.PER_CLASS) μ„€μ •ν•˜λ©΄ static μ•ˆλΆ™μ—¬λ„ 됨
class ExampleTest {

    @BeforeAll
    static void setUpAll() {
        // ν…ŒμŠ€νŠΈ 클래슀의 λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 전에 ν•œ 번만 싀행됨
        System.out.println("Before all tests");
    }

    @BeforeEach
    void setUp() {
        // 각 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 전에 싀행됨
        System.out.println("Before each test");
    }

    @Test
    void test1() {
        System.out.println("Test 1");
    }

    @Test
    void test2() {
        System.out.println("Test 2");
    }

    @AfterEach
    void tearDown() {
        // 각 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 후에 싀행됨
        System.out.println("After each test");
    }

    @AfterAll
    static void tearDownAll() {
        // ν…ŒμŠ€νŠΈ 클래슀의 λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 후에 ν•œ 번만 싀행됨
        System.out.println("After all tests");
    }
    
    //μ‹€ν–‰ κ²°κ³Ό
    Before all tests
	Before each test
	Test 1
	After each test
	Before each test
	Test 2
	After each test
	After all tests
}

Mock Object

그림 좜처

  • Test Double
    • μ‹€μ œ μ˜μ‘΄μ„±μ„ λŒ€μ²΄ν•˜λŠ” 객체
    • ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•΄ μ‹€μ œ κ΅¬ν˜„μ²΄ λŒ€μ‹  μ‚¬μš©λ˜λŠ” κ°€μ§œ 객체
  • Mock
    • 사전에 κΈ°λŒ€ν•˜λŠ” 호좜과 λ°˜ν™˜κ°’μ„ μ •μ˜ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰
    • νŠΉμ • λ™μž‘μ„ μˆ˜ν–‰ν•˜λŠ”μ§€ 확인(λ©”μ„œλ“œ 호좜, λ§€κ°œλ³€μˆ˜, λ°˜ν™˜)ν•˜λŠ” ν–‰μœ„ 검증을 μˆ˜ν–‰ν•¨
  • Stub
    • λ©”μ†Œλ“œκ°€ μˆ˜ν–‰λœ ν›„, 객체의 μƒνƒœλ₯Ό ν™•μΈν•˜μ—¬ μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν–ˆλŠ”μ§€λ₯Ό ν™•μΈν•˜λŠ” μƒνƒœ 검증을 μˆ˜ν–‰ν•¨

Mock Object 생성 Test Framework

  • Mockito
  • JMock
  • EasyMock

Mockito μ˜ˆμ‹œ μ½”λ“œ

class UserServiceTest {

    private UserService userService;
    
    @Mock
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userRepository);
    }

    @Test
    @DisplayName("μƒˆλ‘œμš΄ μ‚¬μš©μžλ₯Ό 생성할 수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldCreateNewUser() {
        // given
        User user = new User("john.doe@example.com", "password123");
        when(userRepository.save(user)).thenReturn(true);

        // when
        boolean result = userService.createUser(user);

        // then
        assertThat(result).isTrue(); // 생성 κ²°κ³Όκ°€ true인지 확인
        verify(userRepository).save(user); // userRepository.save() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }

    @Test
    @DisplayName("κΈ°μ‘΄ μ‚¬μš©μžλ₯Ό μ‘°νšŒν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldRetrieveExistingUser() {
        // given
        String email = "john.doe@example.com";
        User existingUser = new User(email, "password123");
        when(userRepository.findByEmail(email)).thenReturn(existingUser);

        // when
        User result = userService.getUserByEmail(email);

        // then
        assertThat(result).isNotNull(); // 쑰회된 μ‚¬μš©μžκ°€ null이 μ•„λ‹Œμ§€ 확인
        assertThat(result).isEqualTo(existingUser); // 쑰회된 μ‚¬μš©μžκ°€ μ˜ˆμƒν•œ μ‚¬μš©μžμ™€ λ™μΌν•œμ§€ 확인
        verify(userRepository).findByEmail(email); // userRepository.findByEmail() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }

    @Test
    @DisplayName("μ‚¬μš©μžλ₯Ό μ‚­μ œν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldDeleteUser() {
        // given
        String email = "john.doe@example.com";
        when(userRepository.deleteByEmail(email)).thenReturn(true);

        // when
        boolean result = userService.deleteUserByEmail(email);

        // then
        assertThat(result).isTrue(); // μ‚­μ œ κ²°κ³Όκ°€ true인지 확인
        verify(userRepository).deleteByEmail(email); // userRepository.deleteByEmail() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }
}

Spring의 JUnit 5

λ‹¨μœ„ ν…ŒμŠ€νŠΈ 지원

@SpringBootTest // Spring Boot μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ»¨ν…μŠ€νŠΈλ₯Ό λ‘œλ“œν•˜μ—¬ ν…ŒμŠ€νŠΈ
@SpringJUnitConfig(classes = {UserService.class, UserRepository.class}) // ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ λΉˆμ„ μ„€μ •
@ActiveProfiles("test") // "test" ν”„λ‘œνŒŒμΌμ— ν•΄λ‹Ήν•˜λŠ” 빈이 λ‘œλ“œλ¨
class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository; // UserRepository의 MockBean을 μ£Όμž…

    @Test
    @DisplayName("μƒˆλ‘œμš΄ μ‚¬μš©μžλ₯Ό 생성할 수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldCreateNewUser() {
        // given
        User user = new User("john.doe@example.com", "password123");
        when(userRepository.save(user)).thenReturn(user);

        // when
        User result = userService.createUser(user);

        // then
        assertThat(result).isNotNull(); // μƒμ„±λœ μ‚¬μš©μžκ°€ null이 μ•„λ‹Œμ§€ 확인
        assertThat(result.getEmail()).isEqualTo(user.getEmail()); // μƒμ„±λœ μ‚¬μš©μžμ˜ 이메일이 μ˜ˆμƒν•œ 이메일과 λ™μΌν•œμ§€ 확인
        verify(userRepository).save(user); // userRepository.save() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }

    @Test
    @DisplayName("κΈ°μ‘΄ μ‚¬μš©μžλ₯Ό μ‘°νšŒν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldRetrieveExistingUser() {
        // given
        String email = "john.doe@example.com";
        User existingUser = new User(email, "password123");
        when(userRepository.findByEmail(email)).thenReturn(existingUser);

        // when
        User result = userService.getUserByEmail(email);

        // then
        assertThat(result).isNotNull(); // 쑰회된 μ‚¬μš©μžκ°€ null이 μ•„λ‹Œμ§€ 확인
        assertThat(result.getEmail()).isEqualTo(email); // 쑰회된 μ‚¬μš©μžμ˜ 이메일이 μ˜ˆμƒν•œ 이메일과 λ™μΌν•œμ§€ 확인
        verify(userRepository).findByEmail(email); // userRepository.findByEmail() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }

    @Test
    @DisplayName("μ‚¬μš©μžλ₯Ό μ‚­μ œν•  수 μžˆμ–΄μ•Ό ν•œλ‹€.")
    void shouldDeleteUser() {
        // given
        String email = "john.doe@example.com";
        User existingUser = new User(email, "password123");
        when(userRepository.findByEmail(email)).thenReturn(existingUser);

        // when
        boolean result = userService.deleteUserByEmail(email);

        // then
        assertThat(result).isTrue(); // μ‚­μ œ κ²°κ³Όκ°€ true인지 확인
        verify(userRepository).delete(existingUser); // userRepository.delete() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ—ˆλŠ”μ§€ 검증
    }
}

톡합 ν…ŒμŠ€νŠΈ 지원

ν†΅ν•©ν…ŒμŠ€νŠΈ(MockMvc)λŠ” λ‹€μŒ κ°•μ˜μ—μ„œ 정리

κ²°λ‘ 

  • λ‹¨μœ„ ν…ŒμŠ€νŠΈλŠ” μ΅œλŒ€ν•œ μŠ€ν”„λ§μ΄ μ•„λ‹Œ, 순수 μžλ°” μ½”λ“œλ‘œλ§Œ μž‘μ„±
    • λ‹¨μœ„ ν…ŒμŠ€νŠΈλŠ” κ°œλ³„ λͺ¨λ“ˆ λ˜λŠ” 클래슀의 λ™μž‘μ„ λ…λ¦½μ μœΌλ‘œ ν…ŒμŠ€νŠΈν•˜λŠ” 것이 λͺ©ν‘œ
    • μ‹€ν–‰ 속도가 λΉ λ₯΄κ³  νŠΉμ • ν”„λ ˆμž„μ›Œν¬μ— μ’…μ†λ˜μ§€ μ•Šκ³ , 격리된 ν™˜κ²½μ—μ„œ ν…ŒμŠ€νŠΈ μˆ˜ν–‰ κ°€λŠ₯
    • 순수 μžλ°” μ½”λ“œλ‘œ μž‘μ„±λœ ν…ŒμŠ€νŠΈλŠ” μžλ™ν™” λ„κ΅¬λ‚˜ CI/CD νŒŒμ΄ν”„λΌμΈμ— μ‰½κ²Œ 톡합 κ°€λŠ₯
  • 톡합 ν…ŒμŠ€νŠΈλŠ” μŠ€ν”„λ§μ΄ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯듀을 ν™œμš©
    • μ‹€μ œ ν™˜κ²½(Spring Bean)κ³Ό μœ μ‚¬ν•œ μ‘°κ±΄μ—μ„œ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰
    • λ‹€μ–‘ν•œ 계측 κ°„μ˜ μƒν˜Έμž‘μš©(컨트둀러)κ³Ό 톡합 검증
    • λ°μ΄ν„°λ² μ΄μŠ€, μ™ΈλΆ€ μ„œλΉ„μŠ€ λ“±κ³Όμ˜ μƒν˜Έμž‘μš© 검증
profile
μ›Ή κ°œλ°œμ— 관심 μžˆμŠ΅λ‹ˆλ‹€.
post-custom-banner

0개의 λŒ“κΈ€