Spring과 테스트 (1) 단위 테스트

굴착드릴·2024년 8월 26일

개요

Unit Test (단위 테스트)

단위 테스트?

단위테스트는 테스트하고자 하는 목적이 되는 특정 모듈만을 테스트합니다.

주로 수학적 알고리즘이 대상으로 되거나 service 계층이 주된 테스트 대상이 됩니다.

Junit5와 assertj가 보통 사용되며 Mocking에 필요한 라이브러리로는 주로 mockito가 사용됩니다.

하나의 모듈?
하나의 모듈은 하나의 class, method가 보통이 되지만 특정 기능을 수행하는 코드의 묶음입니다. 따라서 여러개의 class나 파일이 하나의 모듈을 구성하는 경우도 있습니다.

Spring의 도움을 받지 않는 방법

주요 메소드 및 어노테이션

  • @Mock
    • Mockito에서 모의 객체를 생성하여, 테스트 대상 객체의 의존성을 대체합니다.
  • @InjectMocks
    • 모의 객체를 자동으로 주입하여, 테스트 대상 객체를 초기화합니다.
  • verify
    • 모의 객체의 특정 메서드가 호출되었는지, 얼마나 자주 호출되었는지 등을 검증합니다.
  • when(targetMethod()).thenReturn(Return value)
    • 특정 메소드를 스터빙할 때 사용합니다.

InjectMocks와 의존성 주입

InjectMock으로 생성된 객체에 필요한 의존성은 Mocking된 객체, Null, 실제 객체 이 셋 중 하나가 주입됩니다.

유닛테스트에서는 Spring context가 로드되지 않기 때문에 mocking을 하지 않은 의존성은 null로 주입됩니다.

만약 @Mock 어노테이션으로 필요한 의존성을 설정했다면 mockito 라이브러리는 자동으로 mocking된 객체를 의존성으로 주입합니다.

하지만 필요한 의존성 중 @Mock 어노테이션으로 설정하지 않았다면 의존성으로 null이 주입됩니다.

Q1. @Service annotation이 있는데 의존성 관련 오류는 뜨지 않나요

A1. 발생하지 않습니다 Annotation은 단순히 객체의 metadata에 해당 정보를 추가하는 것 뿐입니다.

Q2. Service A 의존성인 Service B가 Service C를 의존하고 있을 때 Service B를 mocking하면 Service C 역시 mocking이 자동으로 되나요

A2. 아니요 Service B는 mocking되지만 Service C는 null로 주입됩니다.

Q3. Service A 의존성인 Service B를 mocking했을 때 Service A의 method a가 Service B의 b를 호출할 때 b를 반드시 stubbing해야하나요?

A3. Void 타입이 아니라면 stubbing이 필요하지 않을 수 있지만 return 값이 존재하는 경우 stubbing을 해주는 것이 좋습니다. 이는 mocking된 모듈에서 메소드가 호출 될 시 반환 타입에 해당되는 기본값을 return하기 때문입니다.

UserService, UserRepository

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));
    }

    public void createUser(User user) {
        userRepository.save(user);
    }
}

public interface UserRepository extends JpaRepository<User, Long> {
}

@ExtendWith(MockitoExtension.class)

@Mock, @InjectedMock을 사용하려면 해당 어노테이션을 읽어 처리하는 과정이 필요합니다.

Test class에 MockitoExtention.class 사용을 명시하여 mocking을 진행할 수 있습니다.

@BeforeEach 에서 MockitoAnnotations.openMocks(this)를 통해 초기화하면 @ExtendWith(MockitoExtension.class)를 작성하지 않아도 됩니다.

단위 테스트 코드

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 의존성을 모킹

    @InjectMocks
    private UserService userService; // 실제 테스트할 클래스, 모킹된 UserRepository가 주입됨

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this); // Mockito 어노테이션 초기화
    }

    @Test
    public void testFindUserById_UserExists() {
        // Given
        User mockUser = new User();
        mockUser.setId(1L);
        mockUser.setName("John Doe");
        when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser)); // 행위 정의

        // When
        User foundUser = userService.findUserById(1L);

        // Then
        assertNotNull(foundUser);
        assertEquals("John Doe", foundUser.getName());
        verify(userRepository, times(1)).findById(1L); // 메서드 호출 검증
    }

    @Test
    public void testFindUserById_UserNotFound() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.empty()); // 행위 정의

        // When & Then
        assertThrows(UserNotFoundException.class, () -> userService.findUserById(1L)); // 예외 발생 검증
        verify(userRepository, times(1)).findById(1L); // 메서드 호출 검증
    }

    @Test
    public void testCreateUser() {
        // Given
        User mockUser = new User();
        mockUser.setId(1L);
        mockUser.setName("John Doe");

        // When
        userService.createUser(mockUser);

        // Then
        verify(userRepository, times(1)).save(mockUser); // 저장 메서드 호출 검증
    }
}

Spring의 도움을 받는 경우

@SpringBootTest

@SpringBootTest(classes = Service.class)
public class YourServiceTest {

    @Autowired
    private Service service;

    @MockBean
    private Repository repository;
}

다음과 Spring bean에 service class만 등록 후 이를 주입 받는 방법이 있습니다.
하지만 @SpringBootTest의 경우 통합테스트를 위한 테스트이기 때문에 보통 단위테스트에서는 사용되지 않습니다.

다만 필드주입을 사용하는 경우 반드시 SpringBootTest를 통해 주입받아야 합니다.

@Mock vs @MockBean

@MockBean은 Spring의 application context의 도움을 받아 주입받는 것이고 @MockmockitoExtention을 통해 주입을 받는 것 입니다.

@SpringBootTest와 같이 Spring test library의 도움을 받는 경우 @MockBean
을 사용하면 됩니다.

@RestClientTest

때로 Service에서 외부 api 테스트를 진행해야 하는 경우가 있습니다. 이 경우 간단하게 @RestClientTest 어노테이션을 사용해 외부 api를 모킹할 수 있습니다.

@RestClientTest(Service.class)
public class YourServiceTest {

    @Autowired
    private Service service;

    @Autowired
    private MockRestServiceServer server; 

    @Test
    public void testExternalApiCall() {
    	// 외부 API의 예상 요청과 응답 설정
        this.server.expect(requestTo("/external-api"))
                   .andRespond(withSuccess("{\"key\":\"value\"}", MediaType.APPLICATION_JSON));

        // 서비스 메서드 호출 및 결과 검증
        String result = service.callExternalApi();
        assertEquals("expected response", result);
    }
}
profile
두두두두..

0개의 댓글