단위테스트와 Mockito
서론
회사에서 드디어 테스트코드를 작성하는 방침으로 프로젝트를 진행하게 되었다.
전에는 MR하고 프론트랑 합쳐서 테스트를 화면 단에서 버튼 눌러가면서 잘 동작하나 확인하는 느낌으로 결합 테스트를 했지 백엔드 전용으로 코드를 테스트 하지 못해서 통합 테스트랑 단위 테스트랑 별 차이가 없었다. 그저 로컬 환경에서 테스트하냐 데브 환경에서 테스트하냐 라는 차이정도? 그러다 테스트 코드를 도입하게 되었는데 영 감을 못 잡아서 정리해보고자 한다.
테스트 종류
단위 테스트
- 하나의 모듈을 기준 독립적으로 진행되는 가장 작은 단위 테스트
- 하나의 메서드에는 하나의 기능이 들어가듯 하나의 테스트에는 하나의 동작만 테스트하도록 작성 = 가장 작은 단위 테스트니까!
통합 테스트
- 모듈 통합과정에 모듈 간의 호환성 확인을 위한 테스트
- 각각 모듈들이 유기적으로 잘 연결이 되었는지 확인하는 전체적인 통합 테스트
- 요거 통과해야 릴리즈가 가능하다.
단위 테스트의 목적
단위 테스트를 왜 해야하는가?
- 내가 만든 로직 부분만 테스트하기 때문에 빠르게 문제점을 파악할 수 있다.
- 캐시나 데이터베이스 연결없이 빠르게 테스트 가능
- 해당 로직을 수정할 때 기존에 테스트 코드를 작성했다면 테스트하기가 편함
- 가장 좋은 거는 화면 단에서 테스트 하는게 아니라 코드로 테스트하니까 휴면에러도 없고 정직하고 여러번 테스트 할 수 있다.
단위 테스트를 위해 신경써야할 것들
- 1개의 테스트 함수에는 하나의 개념만 테스트 하도록 하여 독립성을 지킨다
- 테스트에서는 다수의 assert를 사용하지 않는 편이 좋다.
- 어디서든 동일하고 빠른 테스트를 추구하자
- 다른 테스트 코드에 영향이 없도록 설계하자.
문제점
- 보통 서비스 로직을 보면 혼자 작동하는 게 아니라 다른 객체에서 데이터 받아서 동작하는 경우가 많음 .. 근데 단위 테스트는 @Test 애노테이션 붙은 놈을 단위로 테스트를 진행하고 해당 객체가 빈 컨테이너에 존재하지 않다 보니까 제대로 테스트를 진행할 수가 없음 ....
Stub
※ Stub
- 빈 컨테이너에 객체를 넣는 것이 아니라 해당 테스트가 동작할 때 동안 가짜 객체 ( Mock )을 만들어서 주입해주고 원하는 결과가 나오도록 설정하는 것을 뜻함
Stub 적용 예시
1) 외부 리소스나 서비스 의존하는 경우 : 디비나 api호출 , 파일 시스템 등 외부 리소스에 접근하는 코드를 테스트할 때
2) 복잡한 객체 또는 동작을 대체하는 경우 : 테스트하려는 코드가 복잡한 객체를 사용하거나 특정 동작 필요로 할 때
public class MyServiceTest {
@Mock
private MyDependency myDependency; // 테스트 대상 코드에서 의존하는 객체
@Test
public void testMyService() {
// Mockito를 사용하여 myDependency 객체의 동작을 stub으로 설정합니다.
// 원하는 기댓값을 설정해서 사용한다.
Mockito.when(myDependency.getData()).thenReturn("Test Data");
// 테스트 대상인 MyService 객체를 생성하고 myDependency를 주입합니다.
MyService myService = new MyService(myDependency);
// MyService의 특정 메서드를 호출하고 반환값을 확인합니다.
String result = myService.processData();
assertEquals("Processed: Test Data", result);
}
}
Mockito
- 아까 적었던 stub을 만들어주는 테스트 프레임워크 : 가짜 객체를 만들어줌
- 복잡한 연관성을 가진 코드를 테스트할 때 독립적인 테스트를 진행하기 위해 가짜 객체를 만들어서 테스트를 진행할 수 있음
애노테이션 종류
1. @Mock
- 해당 테스트 코드와 상호 작용하는 가짜 객체를 만듦
- 가짜 객체라서 무조건 Stub해서 사용해야함 Stub를 객체를 세팅하지 않으면 null이 나옴
public class MyDependency {
public int getValue() {
return 1;
}
}
public class MyService {
private MyDependency myDependency;
public MyService(MyDependency myDependency) {
this.myDependency = myDependency;
}
public int calculate(int value) {
int dependencyValue = myDependency.getValue();
return value * dependencyValue;
}
}
해당 객체에 대한 테스트코드는 아래와 같다
보면 stub안 한 경우는 어짜피 값이 설정 안되어 있어서 결과물로 데이터면 0, 참조형이면 null이 나온다
가짜 객체라 수행할 수가 없음 무조건 stub을 해서 원하는 세팅을 해야 테스트가 가능하다
public class MyServiceTest {
@Mock
private MyDependency myDependency;
private MyService myService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
myService = new MyService(myDependency);
}
@Test
public void testWithoutStubbing() {
// 테스트 실행 (stub 없이)
int result = myService.calculate(5);
// 결과 검증
Assert.assertEquals(0, result);
}
@Test
public void testWithStubbing() {
// stub 설정
Mockito.when(myDependency.getValue()).thenReturn(10);
// 테스트 실행 (stub 후에)
int result = myService.calculate(5);
// 결과 검증
Assert.assertEquals(50, result);
}
}
2. @Spy
- 일부 메서드 호출 혹은 일부 상태를 유지하면서 테스트 수행
- 진짜 객체를 생성해서 쓰는 거다 보니까 stub을 세팅 안 해도 동작이 가능하다. stub을 쓰면 원하는 결과값이 나오도록 세팅할 수가 있고 안 쓰면 원래 객체의 메서드나 필드로 테스트를 진행할 수 있음
public class MyServiceTest {
@Spy
private MyDependency myDependency;
private MyService myService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
myService = new MyService(myDependency);
}
@Test
public void testWithStubbedValue() {
// getValue() 메서드의 반환값을 2로 스텁(stub) 설정
Mockito.doReturn(2).when(myDependency).getValue();
// 테스트 실행
int result = myService.calculate(5);
// 기대값: 5 * 2 = 10
Assert.assertEquals(10, result);
}
@Test
public void testWithoutStubbedValue() {
// getValue() 메서드의 반환값을 스텁하지 않음
// 테스트 실행
int result = myService.calculate(5);
// 기대값: 5 * 1 = 5
Assert.assertEquals(5, result);
}
}
3. InjectMocks
- 테스트 대상 객체의 가짜 객체를 생성
- @Mock이나 @Spy 애노테이션이 붙은 객체를 자신의 멤버 클래스와 일치하면 주입시킴
public class MyServiceTest {
@Mock
private MyDependency myDependency;
@InjectMocks
private MyService myService;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testWithStubbedValue() {
// getValue() 메서드의 반환값을 2로 스텁(stub) 설정
Mockito.when(myDependency.getValue()).thenReturn(2);
// 테스트 실행
int result = myService.calculate(5);
// 기대값: 5 * 2 = 10
Assert.assertEquals(10, result);
}
@Test
public void testWithoutStubbedValue() {
// getValue() 메서드의 반환값을 스텁하지 않음
// 테스트 실행
int result = myService.calculate(5);
// 기대값: 5 * 1 = 5
Assert.assertEquals(5, result);
}
}
@RunWith(MockitoJunitRunner.class)
mockito에서 제공하는 기능을 사용하기 위해서 테스트 클래스에 달아주는 애노테이션
없는 경우 테스트 코드에 아래의 코드를 삽입해야한다.
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
myService = new MyService(myDependency);
}
참고
https://cornswrold.tistory.com/369
https://lemontia.tistory.com/951
https://mangkyu.tistory.com/145