시작하기에 앞서..😁
단위 테스트
를 위해 모의 객체
를 생성하고 관리하는 데 사용되는 Java 오픈소스 프레임워크를 말한다.
JUnit
에서 가짜 객체인 Mock객체
를 생성해주고 관리하고 검증할 수 있도록 지원해주는 Framework이다.
service의 메서드를 테스트하기 위해서는 service 객체를 생성할 때 필요한 repository를 전달해 줘야 한다. 이런 때 Mockito를 사용한다고 보면 된다.
개발한 프로그램을 테스트할 때 테스트를 수행할 모듈과 연결되는 다른 모듈을 흉내 내는 가짜 모듈을 생성하는 객체이다.
분리되기 어려운 클래스들을 가짜객체를 통해 분리한다.
여기서 분리되기 어려운 클래스들의 예시로는 Controller, Service, Repository가 있다.
Controller 클래스를 테스트하기 위해서는 Controller, Service, Repository를
Service 클래스를 테스트하기 위해서는 Service, Repository를
Repository 클래스를 테스트하기 위해서는 Repository를 테스트해야 한다.
뒤에 딸려오는 클래스를 가짜객체(Mock Object)를 통해 분리해 준다.
Service 클래스를 테스트하기 위해 MockRepository를 만들어 주는 것이다.
겉만 같은 가짜 repository인 것이다.
모의 객체 생성 → 메서드 호출 예상 동작 설정 → 메서드 호출 검증
예상 동작
을 정의합니다.// 1. 모의 객체 생성 : Mock
List<String> mockList = Mockito.mock(List.class);
// 2. 메서드 호출 예상 동작 설정 : Stub
Mockito.when(mockList.size()).thenReturn(5);
// 3. 메서드 호출 검증 : Verify
Mockito.verify(mockList).add("item");
용어 | 분류 | 설명 |
---|---|---|
Mock | 용어 | 실제 객체를 대신하여 프로그래밍을 테스트할 수 있는 모의 객체를 생성하는 것으로 특정 동작이나 결과를 설정하고 검증하기 위해 사용 |
Stub | 용어 | 특정 메서드 호출에 대해 미리 정의된 동작을 반환하는 객체로, 테스트에서 사용될 때 실제 동작이 아닌 가짜 동작을 수행 |
Spy | 용어 | 실제 객체를 사용하면서 해당 객체의 일부 동작을 감시하고 기록할 수 있는 객체 |
Mocking | 용어 | 특정 동작이나 결과를 시뮬레이션하기 위해 모의 객체를 생성하거나 가짜 동작을 정의하는 것 |
Verification | 용어 | 메서드 호출이나 객체 동작이 예상대로 수행되었는지 확인하는 작업 |
Matchers | 메서드 | 모킹이나 확인 작업에서 사용되는 매개변수 일치 여부를 확인하는 데 사용되는 메서드를 제 |
MockitoAnnotations | 클래스 | Mockito 애너테이션을 사용하여 Mock 및 Spy 객체를 초기화하는 데 사용되는 클래스 |
MockitoJUnitRunner | 클래스 | JUnit 테스트에 Mockito를 사용하는 데 필요한 설정을 자동으로 처리하는 러너 클래스 |
MockitoJUnitRunner.Silent | 클래스 | MockitoJUnitRunner와 유사하지만, Mock 객체를 생성하지 않은 클래스에서도 실행 |
실제 객체를 대체하는 ‘모의 객체’로 기대하는 동작을 설정하고 검증을 위해 사용
이러한 Mock로 모의 객체를 생성하고 Stub로 예상 동작을 정의하며 verfiy를 통해 검증을 수행하는 Mockito의 수행과정
실제 객체의 동작을 제어하는 방식으로 동작을 시뮬레이션하는 객체
메서드 호출의 예상 동작을 정의하고 올바르게 호출되었는지 확인
// Mock 객체 생성 : Mock - null의 값을 가지는 리스트가 생성됩니다.
List<String> mockList = Mockito.mock(List.class);
// Mock 객체의 동작 정의
Mockito.when(mockList.size()).thenReturn(5);
// Mock 객체 사용
int size = mockList.size(); // 5를 반환
// 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);
기존 객체의 일부 메서드를 원본 동작을 유지하면서 변경하거나 감시할 수 있게 해주는 기능을 제공
Spy를 사용하면 실제 객체를 생성하고 원하는 메서드를 호출 가능
이는 테스트 도중에 객체의 일부 동작을 감시하고, 특정 메서드 호출을 확인하거나 원하는 대로 메서드의 반환 값을 변경할 수 있는 유연성을 제공
// 실제 객체 생성
List<String> originalList = new ArrayList<>();
// Spy 객체 생성 : Spy - 실제 객체의 원본을 유지하며 객체를 생성합니다.
List<String> spyList = Mockito.spy(originalList);
// Spy 객체의 메서드 동작 정의 : Spy - 이러한 객체에 값을 지정합니다.
Mockito.doReturn("Mocked").when(spyList).get(0);
// Spy 객체 사용
String element = spyList.get(0); // "Mocked"를 반환
모의 객체의 생성을 통해 실제 객체의 동작을 모방하는 모의 객체를 만드는 과정을 의미
이러한 모의객체는 테스트 중인 코드를 격리하고 다양한 시나리오를 시뮬레이션하는 데 사용
// 예시 클래스
public class ExampleClass {
public String getData() {
// 실제 동작을 하는 메소드
return "Real data";
}
}
// Mockito를 사용한 모의 객체 생성 예시
ExampleClass mockExample = Mockito.mock(ExampleClass.class);
// 모의 객체의 동작을 메소드의 반환 값으로 지정
Mockito.when(mockExample.getData()).thenReturn("Mocked data");
// 모의 객체를 사용하여 테스트
String result = mockExample.getData();
// 출력: "Mocked data"
System.out.println(result);
// 모의 객체의 메소드 호출 확인
Mockito.verify(mockExample).getData();
모의 객체의 메서드 호출을 확인하는 프로세스를 의미
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();
Annotation | 설명 |
---|---|
@Mock | 모의 객체(Mock Object)를 생성하는데 사용됩니다. |
@Spy | 스파이 객체(Spy Object)를 생성하는데 사용됩니다. |
@Captor | 모의 객체에 전달된 메서드 인수를 캡처하는데 사용됩니다. |
@InjectMocks | 테스트 대상이 되는 객체에 ‘모의 객체를 자동으로 주입’할때 사용이 됩니다. |
@MockBean | 스프링 프레임워크에서 사용되며, 테스트용 Mock 객체를 생성하고 주입하는 데 사용됩니다. |
@MockitoSettings | Mockito의 설정을 변경하거나 커스터마이즈할 때 사용됩니다. |
@MockitoJUnitRunner | JUnit 테스트에서 Mockito를 사용하기 위해 실행할 때 사용됩니다. |
@BDDMockito | BDD 스타일의 테스트를 위해 Mockito를 사용할 때 사용됩니다. |
@InjectMocks, @Mock는 Mockito에서 유닛 테스트
에 사용되고 @MockBean는 Spring Boot에서 통합 테스트
에서 사용이 됩니다.@Mock는 특정 클래스나 인터페이스에 대해 ‘모의 객체를 생성’하는 역할을 수행
@InjectMocks는 테스트 대상 객체에 모의 객체를 주입
하는 역할을 수행
@Mock와 같이 ‘모의 객체를 생성’한다는 것은 실제 객체와 동일한 메서드와 동작을 가지지만 실제 데이터나 외부 리소스와의 상호작용은 없다.
@InjectMocks와 같이 ‘모의 객체를 주입’하는다는 것은 테스트의 대상이 특정 모의 객체를 사용해야 할 때, 그 모의 객체를 자동으로 주입하여 테스트를 수행할 수 있도록 한다.
모의 객체를 주입하는 것은 @Mock로 생성한 모의 객체가 자동으로 주입되어 테스트가 진행된다.
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.MockBean;
import org.mockito.MockitoSettings;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.BDDMockito;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
public class ExampleClassTest {
@Mock
private Dependency dependencyMock;
@Spy
private Dependency dependencySpy;
@Captor
private ArgumentCaptor<String> captor;
@InjectMocks
private ExampleClass exampleClass;
@MockBean
private ExampleBean exampleBeanMock;
@MockitoSettings
private MockitoSettings settings;
@RunWith(MockitoJUnitRunner.class)
public class ExampleTestClass {
@Test
public void exampleTest() {
// Given
given(dependencyMock.getSomeValue()).willReturn("Mocked value");
// When
String result = exampleClass.doSomething();
// Then
verify(dependencySpy).doSomethingElse();
verify(exampleBeanMock).processValue(captor.capture());
String capturedValue = captor.getValue();
}
}
public class Dependency {
public String getSomeValue() {
return "Real value";
}
public void doSomethingElse() {
// ...
}
}
public interface ExampleBean {
void processValue(String value);
}
public class ExampleClass {
private Dependency dependency;
private ExampleBean exampleBean;
public ExampleClass(Dependency dependency, ExampleBean exampleBean) {
this.dependency = dependency;
this.exampleBean = exampleBean;
}
public String doSomething() {
String value = dependency.getSomeValue();
dependency.doSomethingElse();
exampleBean.processValue(value);
return value;
}
}
}
테스트할 메서드를 호출하는 부분
이 부분에서는 실제로 테스트 대상 메서드를 호출하여 원하는 ‘동작을 검증’
‘테스트 결과를 검증’하는 부분
예상되는 결과를 검증하고, 모의 객체의 메서드가 예상대로 호출되었는지를 확인할 수 있다.
// Given
Mockito.when(mockObject.someMethod()).thenReturn(expectedValue);
// When
Object result = testedObject.testedMethod();
// Then
assertEquals(expectedValue, result);
Mockito.verify(mockObject).someMethod();
테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스을 단축하기 위해
ontroller 테스트는 그저 간단하게 client에게 HTTP Status를 잘 응답하고 있는 지를 테스트하기 위함인데 RestAssured와 @SpringbootTest를 이용하는 것은 그다지 좋은 생각은 아니다.
service의 내부 메서드를 알아야 하거나 테스트를 위해 db에 데이터를 save하거나 로그인을 통해 토큰을 받아오는 번거로운 작업을 mocking 함으로써 테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스를 단축하는 것이 맞다는 판단 하에 controller를 mockito를 이용해 테스트하고 있다.
doThrow(NotFoundCrewException.class).when(reservationService)
.findCrewHistoryByCoach(anyLong());
위와 같이 NotFoundCrewException.class 로 하면 예외 상황에서 HTTP 상태 코드는 잘 오지만 응답 Body값의 칼럼 정보인 message가 null로 반환된다.
doThrow(new NotFoundCrewException()).when(reservationService)
.findCrewHistoryByCoach(anyLong());
new NotFoundCrewException() 로 하면 message 값 확인이 가능하다.
api의 header에 jwt token을 받도록 api가 수정되었다. 아래와 같이 token에 임의 값을 넣고 테스트를 돌리면 401에러가 발생했다.
@Test
void coachFindCrewReservations_invalidCrewId() throws Exception {
String token = "token";
mockMvc.perform(get("/api/v2/crews/a/reservations")
.header("Authorization", "Bearer " + token))
.andDo(print())
.andExpect(status().isBadRequest());
}
테스트가 실패한 이유는 메서드의 실행 흐름에 검증하는 로직이 있었기 때문이였는데 mock 테스트를 할 때는 실행 흐름에 검증하는 로직이 있다면 이것에 대해 default로 에러를 내던가 false를 반환한다.
따라서,
@Test
void coachFindCrewReservations_invalidCrewId() throws Exception {
String token = "token";
given(jwtTokenProvider.validateToken(token))
.willReturn(true);
mockMvc.perform(get("/api/v2/crews/a/reservations")
.header("Authorization", "Bearer " + token))
.andDo(print())
.andExpect(status().isBadRequest());
}
위의 코드와 같이 given().willReturn()
으로 이를 처리해야 다름 실행 순서로 넘어가 우리가 유도하는 흐름대로 테스트가 동작하게 된다.
여기서 궁금증이 생겼다. given()이 Mockito를 import하는 것이 아니라 아니라 BDDMockito
import하고 있었다는 것.... 그럼 BDDMockito는 무엇일까??
Behavior-Driven Development의 약자로 행위 주도 개발을 말한다. 테스트 대상의 상태의 변화를 테스트하는 것이고, 시나리오를 기반으로 테스트하는 패턴을 권장한다.
여기서 권장하는 기본 패턴은 Given, When, Then 구조를 가진다.
@Test
void test() {
//given
//when
//then
}
현재 SpringSecurity를 이용하여 Jwt를 받아 인증과 권한을 확인하고 SecurityContext
에 인증/권한 정보를 설정한다. 따라서 테스트에서 인증 과정을 생략하려면 Authentication
객체를 직접 생성해야한다.
나는 TestingAuthenticationToken
라는 테스트 용도로 만들어진 Authentication 구현체를 사용하였다. 이는 사용자 ID, 비밀번호, 권한 등을 직접 지정할 수 있다.
Authentication authentication = new TestingAuthenticationToken("test1@gmail.com", null, "ROLE_USER");
생성한 Authentication 객체를 MockMvc 요청에 추가하려면 SecurityMockMvcRequestPostProcessors
의 authentication()
메서드를 사용하면 된다.
SecurityMockMvcRequestPostProcessors.authentication(Authentication)
메서드를 사용하면, 주어진 Authentication 객체
로 SecurityContext를 설정하고 이를 MockMvc 요청에 추가할 수 있다. 이렇게 하면 해당 요청은 인증된 것처럼 처리되므로, 인증에 의존하는 로직을 테스트할 수 있게 된다.
즉, 이 메서드는 Authentication 객체
를 받아서 SecurityContext를 설정하고 이 SecurityContext를 현재 요청의 보안 컨텍스트로 설정하는거다. 이렇게 함으로써, 해당 요청은 주어진 Authentication 객체에 의해 인증된 것처럼 처리되므로, 인증이 필요한 로직을 테스트할 수 있다
mockMvc.perform(
patch("/members")
// ...
.with(authentication(authentication))
)
[Spring] JUnit & Mockito 기반 Spring 단위 테스트 코드 작성
Spring Boot Mockito 이해하기 : 테스트 흐름 및 사용예시
mock을 왜 사용하고 있었지?