[Spring] Mockito의 모든 것: 단위 테스트를 더 쉽게 만드는 비결
[배경]
모키토는 Java 개발자가 "단위 테스트(Unit Testing)"를 작성할 때 필수적인 도구로 자리 잡고 있습니다. 소프트웨어 개발의 복잡성이 증가함에 따라, 시스템의 다양한 부분을 개별적으로 테스트할 수 있는 능력이 중요해졌습니다. 모키토는 실제 객체의 의존성을 모의(Mock) 객체로 대체하여 코드의 특정 부분을 독립적으로 테스트할 수 있게 해줍니다. 이를 통해 개발자는 외부 종속성에 영향을 받지 않는 신뢰할 수 있는 테스트를 작성할 수 있습니다.
[내용]
1. Mockito의 개념
모키토는 모의 객체를 쉽게 생성하고 조작할 수 있도록 설계된 프레임워크입니다. 모의 객체는 실제 객체를 대체하여 테스트에서 사용되며, 특히 종속성이나 외부 리소스에 의존하지 않는 독립적인 단위 테스트를 작성할 때 유용합니다. 모의 객체를 사용하면 테스트하려는 시스템의 일부 기능을 격리하여 테스트할 수 있으며, 의도하지 않은 부작용을 피할 수 있습니다.
2. Mockito의 특징
- 모키토는 단위 테스트에서 의존성 주입을 활용하여 객체 간의 의존성을 쉽게 관리할 수 있게 해줍니다. 모키토를 사용하면 테스트 대상 객체의 의존성을 모의 객체로 대체하여 테스트 중 실제 데이터베이스 연결이나 외부 서비스 호출을 방지할 수 있습니다. 이를 통해 빠르고 독립적인 테스트 환경을 조성할 수 있습니다.
- 모키토는 간결하고 읽기 쉬운 API를 제공하여, 개발자가 최소한의 코드로 복잡한 테스트 시나리오를 작성할 수 있게 도와줍니다. when, thenReturn, verify와 같은 메서드를 통해 가독성이 좋은 테스트 코드를 작성할 수 있으며, 이는 유지보수와 협업에도 큰 도움이 됩니다.
- 모키토를 사용하면 테스트 코드의 유지보수성을 크게 향상시킬 수 있습니다. 실제 객체의 상태 변화나 외부 요인에 의한 테스트 실패를 줄일 수 있기 때문에, 테스트의 안정성을 높이고 코드의 변화에 유연하게 대응할 수 있습니다. 이는 프로젝트의 장기적인 코드 품질 유지와 관련이 있습니다.
- 모키토는 JUnit과 같은 다른 테스트 프레임워크와 잘 통합되도록 설계되었습니다. 이는 개발자가 기존에 사용하던 테스트 프레임워크와 손쉽게 연동하여 사용할 수 있게 해주며, 테스트 작성에 필요한 학습 비용을 줄여줍니다.
3. Mockito의 동작과정
💡 Mockito 동작과정
- 모의 객체 생성(Mock) → 메서드 호출 예상 동작 설정(Stub) → 메서드 호출 검증(Verify)
1. 모의 객체 생성 : Mock
- 실제 객체를 대체하는 ‘모의 객체’로 기대하는 동작을 설정하고 검증을 위해 사용이 됩니다.
- 이러한 Mock로 모의 객체를 생성하고 Stub로 예상 동작을 정의하며 verfiy를 통해 검증을 수행하는 Mockito의 수행과정입니다.
- 실제 객체의 동작을 제어하는 방식으로 동작을 시뮬레이션하는 객체입니다.
- 메서드 호출의 예상 동작을 정의하고 올바르게 호출되었는지 확인할 수 있습니다.
- Mockito를 사용하여 테스트에 필요한 '모의(가짜) 리스트'를 생성합니다.
- MockList의 경우는 실제 리스트와 동일한 방식으로 동작을 하지만, 이 동작을 사전에 정의 할 수 있습니다.
// Mock 객체 생성 : Mock - null의 값을 가지는 리스트가 생성됩니다.
List<String> mockList = Mockito.mock(List.class);
// Mock 객체의 동작 정의
Mockito.when(mockList.size()).thenReturn(5);
// Mock 객체 사용
int size = mockList.size(); // 5를 반환
2. 메서드 호출 예상 동작 설정 : Stub
- 테스트 중에 모의 객체(Mock Object)의 동작을 정의하는 ‘예상 동작’을 설정하는 기능입니다.
- 이를 사용하여 메서드가 호출될 때 어떤 값을 반환하거나 어떤 예외를 던져야 하는지를 지정할 수 있습니다.
- 이를 통해 종속성의 동작을 제어하고 테스트 대상을 격리할 수 있습니다.
// Mock 객체 생성
List<String> mockList = Mockito.mock(List.class);
// Mock 객체의 동작 정의 : Stub - Mock 객체의 사이즈를 항상 10으로 반환하도록 예상동작을 설정합니다.
Mockito.when(mockList.size()).thenReturn(10);
// Mock 객체의 메소드 호출
int size = mockList.size();
// 결과 확인
assertEquals(10, size);
3. 메서드 호출 검증 : Verify
- 모의 객체의 메서드 호출을 확인하는 프로세스를 의미합니다.
- Verification을 사용하여 특정 메서드가 예상대로 호출되었는지를 확인할 수 있습니다.
- ExampleClass라는 예시 클래스의 methodA()와 methodB()를 모의 객체로 대체하여 해당 메서드의 호출을 확인하고, 호출 순서를 검증할 수 있습니다.
// 예시 클래스
public class ExampleClass {
public void methodA() {
// 동작 A
}
public void methodB() {
// 동작 B
}
}
// Mockito를 사용한 Verification 예시
ExampleClass mockExample = Mockito.mock(ExampleClass.class);
// 메소드 호출
mockExample.methodA();
mockExample.methodB();
// 메소드 호출 확인
Mockito.verify(mockExample).methodA();
Mockito.verify(mockExample).methodB();
// 메소드 호출 순서 확인
InOrder inOrder = Mockito.inOrder(mockExample);
inOrder.verify(mockExample).methodA();
inOrder.verify(mockExample).methodB();
4. 객체의 행동 동작 : Spy
- 기존 객체의 일부 메서드를 원본 동작을 유지하면서 변경하거나 감시할 수 있게 해주는 기능을 제공합니다.
- Spy를 사용하면 실제 객체를 생성하고 원하는 메서드를 호출할 수 있습니다. 이는 테스트 도중에 객체의 일부 동작을 감시하고, 특정 메서드 호출을 확인하거나 원하는 대로 메서드의 반환 값을 변경할 수 있는 유연성을 제공합니다.
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SpyExampleTest {
@Test
public void testSpy() {
List<String> list = new ArrayList<>();
List<String> spyList = Mockito.spy(list);
// 원본 메소드 호출
spyList.add("one");
spyList.add("two");
// Spy 객체의 메소드 호출
Mockito.verify(spyList).add("one");
Mockito.verify(spyList).add("two");
assertEquals(2, spyList.size()); // 실제 객체의 메소드 호출 결과 확인
}
}
spy를 사용하여 list 객체의 spy 생성하고 spy 객체를 통해 원본 리스트에 내용을 "one"과 "two" 를 추가했습니다. 그이후 spy 객체가 진짜로 추가를 했는지 확인하고 객체의 크기를 확인하는 코드입니다.
[결론]
Mockito는 테스트 코드의 가독성과 유지보수성을 향상시킬 수 있는 중요한 도구입니다. 목 객체를 통해 복잡한 의존성을 간단하게 대체하고, 코드의 의도를 명확히 할 수 있습니다. 또, 실제 환경과 독립적으로 코드를 테스트할 수 있으며, 더 다양한 시나리오를 다룰 수 있습니다. 이를 통해 예상치 못한 오류를 사전에 발견하고 해결합니다. Mockito를 잘 활용하면 코드의 신뢰성을 높이고, 프로젝트의 품질을 전반적으로 향상시시킵니다. 이렇게 작성된 테스트 코드는 시간이 지나도 쉽게 이해하고 수정할 수 있어, 장기적으로 프로젝트의 성공에 기여할 것입니다.
[출처 및 참조]