
Mocking이란?
1. Mocking의 개념
Mocking은 테스트를 진행할 때 실제 객체 대신 가짜 객체(Mock 객체)를 생성하여 사용하는 기법입니다. 이 가짜 객체는 실제 객체의 행동을 흉내내어, 테스트 대상 코드가 의존하는 다른 컴포넌트와의 상호작용을 시뮬레이션합니다.
왜 Mocking이 필요한가요?
- 단위 테스트의 독립성 보장: 테스트 대상 코드만 격리하여 테스트할 수 있습니다.
- 외부 의존성 제거: 데이터베이스, 외부 API 등 외부 시스템에 의존하지 않고 테스트할 수 있습니다.
- 테스트 속도 향상: 실제 객체보다 가벼운 Mock 객체를 사용하여 테스트 속도를 높일 수 있습니다.
- 특정 시나리오 테스트: 실제 환경에서 재현하기 어려운 상황(예: 네트워크 오류)을 시뮬레이션할 수 있습니다.
2. 단위 테스트와 통합 테스트의 차이
테스트 방식은 크게 단위 테스트와 통합 테스트로 나눌 수 있습니다. 각각의 특징과 차이점을 이해하는 것이 중요합니다.
단위 테스트(Unit Test)
단위 테스트는 코드의 가장 작은 단위(주로 메서드나 클래스)를 격리하여 테스트하는 방식입니다.
특징:
- 테스트 대상 코드만 격리하여 테스트
- 외부 의존성을 Mock 객체로 대체
- 빠른 실행 속도
- 특정 기능이나 로직에 집중
- 문제 발생 시 원인 파악이 용이
예시:
@ExtendWith(MockitoExtension.class)
class QuestionServiceMockTest {
@Mock
private QuestionRepository questionRepository;
@InjectMocks
private QuestionService questionService;
@Test
public void getQuestionTest() {
// Mock 객체 동작 정의
when(questionRepository.findById(1L))
.thenReturn(Optional.of(mockQuestion));
// 테스트 실행
QuestionDto result = questionService.getQuestion(1L);
// 검증
assertEquals("테스트 제목", result.getSubject());
}
}
통합 테스트(Integration Test)
통합 테스트는 여러 컴포넌트 간의 상호작용을 포함하여 테스트하는 방식입니다.
특징:
- 여러 컴포넌트의 상호작용 테스트
- 실제 객체와 환경 사용(데이터베이스, 외부 API 등)
- 상대적으로 느린 실행 속도
- 전체 시스템의 동작 검증
- 실제 환경과 유사한 조건에서 테스트
예시:
@SpringBootTest
class QuestionServiceTest {
@Autowired
private QuestionService questionService;
@Autowired
private EntityManager em;
@Test
public void createQuestionTest() {
// 실제 서비스 호출
QuestionDto saveQuestion = questionService.createQuestion(questionCreateDto);
// 데이터베이스 상태 반영
em.flush();
em.clear();
// 검증
QuestionDto findQuestion = questionService.getQuestion(saveQuestion.getId());
assertEquals(saveQuestion.getId(), findQuestion.getId());
}
}
현재 프로젝트의 테스트 방식
현재 프로젝트는 다음과 같은 특징을 통해 통합 테스트 방식으로 구현되어 있음을 알 수 있습니다:
- @SpringBootTest 어노테이션 사용: 전체 Spring 컨텍스트를 로드하여 테스트
- @Autowired를 통한 실제 Bean 주입: Mock 객체가 아닌 실제 구현체 사용
- 실제 데이터베이스 연동: EntityManager를 통한 데이터베이스 조작
- Mock 관련 어노테이션 부재: @Mock, @MockBean 등의 어노테이션 미사용
- 테스트 데이터 초기화 방식: 실제 서비스 메서드를 호출하여 데이터 초기화
@sl4j
@SpringBootTest
class QuestionServiceTest {
@Autowired
private QuestionService questionService;
@BeforeEach
public void init() {
// 실제 서비스를 통한 데이터 초기화
QuestionCreateDto questionCreateDto = QuestionCreateDto.from("궁금해요", "이것은 무엇인가요?", "작성자");
questionService.createQuestion(questionCreateDto);
}
// 테스트 메서드...
}
3. Java 프로젝트에서 테스트 코드가 필요한 이유
Java와 같은 정적 타입 언어에서 테스트 코드는 특히 중요합니다. 그 이유는 다음과 같습니다:
1) 버그 조기 발견
컴파일 시점에 발견되지 않는 논리적 오류나 런타임 오류를 테스트를 통해 조기에 발견할 수 있습니다. 이 프로젝트에서 예를 들면:
@Test
@DisplayName("질문 등록 테스트")
public void createQuestionTest() {
// given
QuestionCreateDto questionCreateDto = QuestionCreateDto.from("질문 제목 입니다.", "질문은 무엇인가요?", "작성자");
QuestionDto saveQuestion = questionService.createQuestion(questionCreateDto);
// when
QuestionDto findQuestion = questionService.getQuestion(saveQuestion.getId());
// then
assertEquals(saveQuestion.getId(), findQuestion.getId());
}
이 테스트는 질문 등록 기능이 제대로 동작하는지 확인합니다. 만약 등록 과정에서 문제가 발생한다면, 실제 애플리케이션에 배포되기 전에 이 테스트가 실패하여 문제를 발견할 수 있습니다.
2) 리팩토링 안전성 확보
코드를 리팩토링할 때, 테스트 코드는 기존 기능이 올바르게 동작하는지 확인하는 안전망 역할을 합니다. 예를 들어, QuestionService
클래스의 내부 구현을 변경하더라도 테스트가 통과한다면 기능이 제대로 동작한다고 확신할 수 있습니다.
3) 문서화 역할
테스트 코드는 코드의 사용 방법과 예상 동작을 보여주는 살아있는 문서 역할을 합니다. 이 프로젝트에서 AnswerServiceTest
는 답변 생성 방법을 명확하게 보여줍니다:
@Test
@DisplayName("답변 등록 테스트")
public void createAnswerTest() {
// given
QuestionCreateDto questionCreateDto = QuestionCreateDto.from("질문 제목 입니다.", "질문은 무엇인가요?", "작성자");
QuestionDto question = questionService.createQuestion(questionCreateDto);
// when
AnswerCreateDto answerCreateDto = AnswerCreateDto.from(question.getId(), "답변 내용", "작성자");
AnswerDto answer = answerService.createAnswer(answerCreateDto);
// then
assertDoesNotThrow(() -> answerService.getAnswer(answer.getId()));
}
4) 설계 개선
테스트 작성 과정에서 코드의 설계 문제가 드러나는 경우가 많습니다. 테스트하기 어려운 코드는 대개 설계가 좋지 않은 경우가 많으며, 테스트를 작성하면서 더 나은 설계로 개선할 수 있습니다.
5) 협업 효율성 증가
팀 프로젝트에서 테스트 코드는 다른 개발자가 작성한 코드를 이해하고 수정하는 데 도움을 줍니다. 이 프로젝트에서 새로운 개발자가 합류하더라도 테스트 코드를 통해 시스템의 동작 방식을 빠르게 이해할 수 있습니다.
4. Spring Boot에서의 Mocking
Spring Boot 애플리케이션에서는 주로 Mockito 라이브러리를 사용하여 Mocking을 구현합니다. 이 프로젝트에서도 테스트 코드에서 Mocking을 활용할 수 있습니다.
주요 Mockito 어노테이션
- @Mock: 가짜 객체를 생성합니다.
- @InjectMocks: @Mock으로 생성된 객체를 자동으로 주입받는 객체를 생성합니다.
- @MockBean: Spring 컨텍스트에서 Bean을 Mock 객체로 대체합니다.
- @SpyBean: 실제 객체의 일부 메서드만 Mocking합니다.
5. 프로젝트 예시로 Mocking 이해하기
현재 프로젝트에서는 통합 테스트 방식으로 구현되어 있어 Mocking을 직접 사용하지 않고 있습니다. 하지만 단위 테스트를 위해 Mocking을 적용한다면 다음과 같이 구현할 수 있습니다.
QuestionService 단위 테스트 예시 (Mocking 적용)
@ExtendWith(MockitoExtension.class)
class QuestionServiceMockTest {
@Mock
private QuestionRepository questionRepository;
@InjectMocks
private QuestionService questionService;
@Test
@DisplayName("질문 조회 테스트 (Mocking)")
public void getQuestionTest() {
// given
Long questionId = 1L;
Question mockQuestion = Question.of(
"테스트 제목",
"테스트 내용",
"작성자",
QuestionStatus.ING
);
mockQuestion.setId(questionId);
// Mock 객체의 동작 정의
when(questionRepository.findById(questionId))
.thenReturn(Optional.of(mockQuestion));
// when
QuestionDto result = questionService.getQuestion(questionId);
// then
assertEquals(questionId, result.getId());
assertEquals("테스트 제목", result.getSubject());
// Mock 객체의 메서드가 호출되었는지 검증
verify(questionRepository).findById(questionId);
}
}
AnswerService 단위 테스트 예시 (Mocking 적용)
@ExtendWith(MockitoExtension.class)
class AnswerServiceMockTest {
@Mock
private AnswerRepository answerRepository;
@Mock
private QuestionRepository questionRepository;
@InjectMocks
private AnswerService answerService;
@Test
@DisplayName("답변 생성 테스트 (Mocking)")
public void createAnswerTest() {
// given
Long questionId = 1L;
Question mockQuestion = Question.of(
"테스트 제목",
"테스트 내용",
"작성자",
QuestionStatus.ING
);
mockQuestion.setId(questionId);
AnswerCreateDto answerCreateDto = AnswerCreateDto.from(
questionId,
"답변 내용",
"답변 작성자"
);
Answer mockAnswer = Answer.createAnswer(
"답변 내용",
"답변 작성자",
AnswerStatus.NOT_ACCEPTED
);
// Mock 객체의 동작 정의
when(questionRepository.findById(questionId))
.thenReturn(Optional.of(mockQuestion));
when(answerRepository.save(any(Answer.class)))
.thenReturn(mockAnswer);
// when
AnswerDto result = answerService.createAnswer(answerCreateDto);
// then
assertEquals("답변 내용", result.getContent());
assertEquals("답변 작성자", result.getAuthor());
// Mock 객체의 메서드가 호출되었는지 검증
verify(questionRepository).findById(questionId);
verify(answerRepository).save(any(Answer.class));
}
}
6. Mocking의 장단점
장점
- 테스트 대상 코드를 격리하여 순수하게 테스트할 수 있습니다.
- 외부 의존성 없이 테스트를 실행할 수 있어 테스트 속도가 빠릅니다.
- 특정 시나리오를 쉽게 테스트할 수 있습니다.
단점
- Mock 객체 설정이 복잡할 수 있습니다.
- 실제 환경과 다른 동작을 할 수 있어 통합 테스트도 함께 수행해야 합니다.
- Mock 객체의 동작을 잘못 정의하면 테스트가 실제 문제를 발견하지 못할 수 있습니다.
7. 테스트 커버리지 70%의 의미
테스트 커버리지는 테스트 코드가 프로덕션 코드를 얼마나 실행하는지를 측정하는 지표입니다. 커버리지 70%는 전체 코드의 70%가 테스트에 의해 실행된다는 의미입니다.
커버리지의 종류
- 라인 커버리지(Line Coverage): 코드의 각 라인이 테스트에 의해 실행되는 비율
- 분기 커버리지(Branch Coverage): 코드의 모든 조건 분기(if-else 등)가 테스트되는 비율
- 함수 커버리지(Function Coverage): 정의된 함수가 호출되는 비율
- 클래스 커버리지(Class Coverage): 정의된 클래스가 인스턴스화되는 비율
이 프로젝트에서의 70% 커버리지 예시
이 프로젝트에서 70% 커버리지를 달성한다면, 다음과 같은 의미를 가집니다:
public class QuestionService {
// 테스트 커버리지에 포함된 메서드 (70%)
public QuestionDto getQuestion(Long id) { ... }
public QuestionDto createQuestion(QuestionCreateDto questionCreateDto) { ... }
public QuestionDto updateQuestion(QuestionUpdateDto questionUpdateDto) { ... }
// 테스트 커버리지에 포함되지 않은 메서드 (30%)
public void deleteQuestion(Long id) { ... }
private void validateQuestion(QuestionCreateDto questionCreateDto) { ... }
}
위 예시에서 getQuestion()
, createQuestion()
, updateQuestion()
메서드는 테스트에 의해 실행되지만, deleteQuestion()
과 validateQuestion()
메서드는 테스트되지 않았습니다.
커버리지 70%의 중요성
- 품질 지표: 70%는 일반적으로 양호한 수준의 테스트 커버리지로 간주됩니다. 이는 코드의 대부분이 테스트되고 있음을 의미합니다.
- 균형점: 100% 커버리지를 달성하는 것은 비용과 시간 측면에서 효율적이지 않을 수 있습니다. 70%는 테스트 노력과 비용 대비 효과의 균형점으로 볼 수 있습니다.
- 중요 코드 집중: 70% 커버리지를 달성할 때, 비즈니스 로직이나 복잡한 알고리즘과 같은 중요한 코드에 테스트를 집중하는 것이 중요합니다.
커버리지 측정 도구
Java 프로젝트에서는 JaCoCo(Java Code Coverage)와 같은 도구를 사용하여 테스트 커버리지를 측정할 수 있습니다. 이 도구는 테스트 실행 후 커버리지 리포트를 생성하여 어떤 부분이 테스트되지 않았는지 시각적으로 보여줍니다.
// build.gradle에 JaCoCo 설정 예시
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.7"
}
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
}
}
test {
finalizedBy jacocoTestReport
}
8. 단위 테스트와 통합 테스트의 균형
효과적인 테스트 전략은 단위 테스트와 통합 테스트의 적절한 균형을 유지하는 것입니다. 두 가지 테스트 방식은 서로 보완적인 관계에 있습니다.
테스트 피라미드
테스트 피라미드는 효과적인 테스트 전략을 시각화한 개념입니다:
- 단위 테스트: 빠르고 가벼우며, 많은 수의 테스트 케이스를 작성
- 통합 테스트: 중간 수준의 테스트로, 컴포넌트 간 상호작용 검증
- UI/E2E 테스트: 느리고 무거우며, 전체 시스템 동작 검증을 위한 소수의 테스트
현재 프로젝트에 적용할 수 있는 전략
현재 프로젝트는 통합 테스트 위주로 구현되어 있습니다. 다음과 같은 전략으로 단위 테스트를 추가할 수 있습니다:
- 핵심 비즈니스 로직에 단위 테스트 추가: 복잡한 비즈니스 로직이나 알고리즘에 대해 Mocking을 활용한 단위 테스트 추가
- 외부 의존성이 많은 부분에 Mocking 적용: 외부 API 연동이나 데이터베이스 접근이 많은 부분에 Mocking을 적용하여 테스트 속도 향상
- 기존 통합 테스트 유지: 컴포넌트 간 상호작용을 검증하는 통합 테스트는 유지하여 전체 시스템의 동작 검증
- 테스트 커버리지 모니터링: JaCoCo와 같은 도구를 사용하여 테스트 커버리지를 모니터링하고, 중요한 코드에 대한 테스트 집중
9. 결론
Mocking은 단위 테스트를 효과적으로 수행하기 위한 중요한 기법입니다. 실제 객체 대신 가짜 객체를 사용함으로써 테스트 대상 코드를 격리하고, 외부 의존성 없이 테스트할 수 있습니다.
Java 프로젝트에서 테스트 코드는 버그 조기 발견, 리팩토링 안전성 확보, 문서화, 설계 개선, 협업 효율성 증가 등 다양한 이점을 제공합니다. 테스트 커버리지 70%는 코드의 대부분이 테스트되고 있음을 의미하며, 테스트 노력과 효과의 균형점으로 볼 수 있습니다.
현재 프로젝트에서는 통합 테스트 방식을 사용하고 있지만, 단위 테스트의 장점을 활용하기 위해 Mocking을 적용한 테스트 코드를 추가하고 적절한 테스트 커버리지를 맞춰 균형을 유지하는 것이 효과적인 테스트 전략의 핵심입니다.
'개발' 카테고리의 다른 글
Spring Bean이란? (0) | 2025.03.22 |
---|---|
Kafka와 RabbitMQ 비교 분석 (0) | 2025.03.21 |
Kafka+Docker 기반 SpringBoot 프로젝트 구축 방법 (0) | 2025.03.20 |
Spring Boot - Builder 패턴 (0) | 2025.03.12 |
Spring Boot에 Mysql Docker 연결 (0) | 2024.05.14 |
IT/보안