어떻게 테스트 할 것인가

od·2025년 5월 16일

System Design

목록 보기
6/10

테스트는 서비스의 품질을 결정짓는 중요한 요소 입니다.
효과적인 테스트는 사전에 오류를 방지하고 서비스 품질을 체계적으로 보증하는데 필수적인 역할을 합니다.
이러한 테스트코드를 작성하는 데에도 기준과 전략이 필요합니다.


단위 테스트

단위 테스트(Unit Test)는 코드의 가장 작은 단위(메서드, 클래스 등)가 예상대로 동작하는지 검증하는 테스트입니다. 이 테스트의 핵심은 외부 의존성을 배제하고 테스트하려는 로직만을 독립적으로 검증하는 것입니다. POJO 테스트가 이에 해당하며 외부 의존성이 필요한 경우 대역 객체(Mock 객체 등)를 사용하여 테스트를 진행합니다.

Test Double

외부 의존성이 필요한 메소드를 단위테스트 하는 경우 실제 의존성을 주입하는 것이 아니라 대역 객체 (Test Double) 를 선언하여 사용합니다. 대역 객체는 다음의 다섯 가지가 있습니다.

대역객체TypeDescription
Dummy의존성으로 필요하지만 아무런 동작을 하지 않는 객체
주로 메소드나 생성자의 매개변수로 전달됨
Stub상태 검증특정 입력에 대한 고정된 결과를 반환하는 경우 사용되는 객체
Fake상태 검증실제 구현과 비슷한 동작을 하면서 테스트 용으로 단순화한 구현체
Mock행위 검증인터페이스나 의존성 객체의 입력값에 따른 호출유무와 응답값을 검증
Spy행위 검증실제 객체를 감싸서 행위하게 만들고 그 행위를 검증하는 객체



상태 검증과 행위 검증

대역 객체는 크게 상태 검증 객체와 행위 검증 객체로 구분됩니다.
상태 검증 객체는 테스트 대상 객체가 의존하는 인터페이스를 상속한 Fake 클래스를 직접 구현하고 이를 주입하여 테스트를 수행합니다. 행위 검증 객체는 주로 Mockito와 같은 외부 라이브러리를 사용하여 특정 메서드가 호출되었는지 여부나 호출 횟수 등을 검증합니다.

행위 검증은 테스트 코드에서 메서드의 동작을 하나하나 명시적으로 선언해야 하기 때문에 코드가 복잡하고 가독성이 떨어질 수 있는 단점이 있습니다. 따라서 행위에 대한 검증이 꼭 필요한 경우가 아니라면 상태 검증 방식이 성능과 가독성 측면에서 더 유리합니다.

public interface UserRepository {
    Optional<User> findById(Long id);
}

public class UserService {
    private final UserRepository userRepository;

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

    public User getUser(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }
}
public class FakeUserRepository implements UserRepository {

    private final Map<Long, User> store = new HashMap<>();

    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    // 테스트용 유저 저장 메서드
    public void save(User user) {
        store.put(user.getId(), user);
    }
}

public class UserServiceTest {

    @Test
    void 사용자_조회_상태검증() {
        // given
        FakeUserRepository userRepository = new FakeUserRepository();
        userRepository.save(new User(1L, "od"));
        UserService userService = new UserService(userRepository);

        // when
        User result = userService.getUser(1L);

        // then
        assert result.getName().equals("od"); // 상태 검증만 수행
    }
}
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void 사용자_조회_테스트() {
        // given
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(new User(1L, "od")));

        // when
        User result = userService.getUser(1L);

        // then
        verify(userRepository, times(1)).findById(1L); // 행위 검증
        assertEquals("od", result.getName());          // 상태 검증
    }
}





통합 테스트

통합 테스트(Integration Test)는 여러 모듈이나 컴포넌트가 상호작용하면서 전체 시스템이 기대한 대로 동작하는지 확인하는 테스트입니다. 이 테스트의 주요 목적은 개별 모듈의 동작뿐만 아니라 모듈 간의 상호작용을 통해 시스템 전체가 정상적으로 작동하는지를 검증하는 것입니다.

통합 테스트에서는 실제 스프링 컨테이너를 띄운 후 애플리케이션의 다양한 구성 요소들을 실제 환경처럼 연결하여 테스트를 수행합니다. 이러한 테스트는 외부 의존성(예: 데이터베이스, 메시지 큐 등)과의 연결이 정확하게 이루어졌는지, 비즈니스 로직이 의도한 대로 동작하는지를 검증합니다. 일반적으로 @SpringBootTest 나 @DataJpaTest와 같은 어노테이션을 사용하여 실제 애플리케이션과 외부 시스템과의 통합을 테스트합니다.

@SpringBootTest
public class UserServiceTest {

	@Autowired
	private UserService userService;
	
    @Test
    void 사용자_조회_상태검증() {
    
        // given
        Long userId = 1L;

        // when
        User result = userService.getUser(userId);

        // then
        assert result.getName().equals("od");
    }
}





테스트 전략

헥사고날 아키텍처에서 out port에 대한 테스트는 통합 테스트로 진행하는 것이 일반적입니다. out port는 외부 시스템과의 상호작용을 담당하는 인터페이스로 데이터베이스, REST API, 메시지 큐와의 연동을 처리하는 부분입니다. 이러한 out port는 반드시 통합 테스트를 통해 실제 외부 시스템과의 연결을 검증해야 하며 외부 시스템이 정상적으로 동작하는지, 애플리케이션이 외부 의존성과 올바르게 상호작용 하는지를 확인하는 중요한 테스트입니다.

반면 단위 테스트는 외부 의존성을 Fake 클래스나 Mock 객체로 대체하여 비즈니스 로직만을 테스트합니다. 외부 의존성을 배제하고 애플리케이션의 핵심 로직을 독립적으로 검증할 수 있습니다. 만약 Fake 클래스에 로그를 기록한 후 OutputCaptureExtension 을 사용하면 행위에 대한 검증도 가능하게 됩니다.

통합 테스트에서 @SpringBootTest 어노테이션을 사용하면 매번 새로운 스프링 컨테이너가 생성되기 때문에 테스트의 성능에 영향을 미칠 수 있습니다. 이를 해결하기 위해 IntegrationTestSupport 클래스를 작성하고 이 클래스를 테스트에서 상속받도록 하여 하나의 스프링 컨테이너만 생성되도록 관리하는 방식이 성능 측면에서 유리합니다.

또한 persistence out port를 테스트할 때는 실제 프로덕션 환경의 외부 의존성과 상호작용 하지 않도록 H2 Database, Embedded Redis, Embedded Kafaka 등을 사용하는 것이 안전합니다. 이는 테스트 환경에서 프로덕션 환경과 중복되는 부분을 피할 수 있으며, 빠르게 데이터를 준비하고 검증할 수 있는 이점이 있습니다. H2 데이터베이스를 사용하면 테스트용 데이터베이스를 별도로 설정할 수 있어 프로덕션 환경과의 충돌을 방지할 수 있습니다.

@SpringBootTest 
@ActiveProfiles("test") 
@AutoConfigureMockMvc
public class IntegrationTestSupport {  

	@Autowired  
	protected MockMvc mockMvc;
	
	@Autowired  
	protected ObjectMapper objectMapper;
}
profile
차분하게 단단히 쌓아가는 개발자

0개의 댓글