[Spring Boot] Mock? Mockito??

10000JI·2024년 3월 18일
0

Spring Boot

목록 보기
15/15
post-thumbnail

시작하기에 앞서..😁

단위테스트가 뭔데!

  • 단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트를 말한다.
  • 하나의 모듈이란 각 계층에서의 하나의 기능 또는 메소드로 이해할 수 있다.
    하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것이다.

그렇다면 통합테스트는?

  • 모듈을 통합하는 과정에서 모듈 간 호환성을 확인하기 위한 테스트다.
  • 다른 객체들과 데이터를 주고받으며 복잡한 기능이 수행 될때, 연관된 객체들과 올바르게 동작하는지 검증하고자 하는 테스트다.
  • 독립적인 기능보다 전체적인 연관 기능과 웹 페이지로 부터 API를 호출하여 올바르게 동작하는지 확인한다.

Mockito란?🍕

단위 테스트를 위해 모의 객체를 생성하고 관리하는 데 사용되는 Java 오픈소스 프레임워크를 말한다.

  • JUnit에서 가짜 객체인 Mock객체를 생성해주고 관리하고 검증할 수 있도록 지원해주는 Framework이다.

  • service의 메서드를 테스트하기 위해서는 service 객체를 생성할 때 필요한 repository를 전달해 줘야 한다. 이런 때 Mockito를 사용한다고 보면 된다.

  • mockito Github

Mock object(가짜 객체)🍕

  • 개발한 프로그램을 테스트할 때 테스트를 수행할 모듈과 연결되는 다른 모듈을 흉내 내는 가짜 모듈을 생성하는 객체이다.

    분리되기 어려운 클래스들을 가짜객체를 통해 분리한다.

    여기서 분리되기 어려운 클래스들의 예시로는 Controller, Service, Repository가 있다.

  • Controller 클래스를 테스트하기 위해서는 Controller, Service, Repository를

    Service 클래스를 테스트하기 위해서는 Service, Repository를

    Repository 클래스를 테스트하기 위해서는 Repository를 테스트해야 한다.

  • 뒤에 딸려오는 클래스를 가짜객체(Mock Object)를 통해 분리해 준다.

    Service 클래스를 테스트하기 위해 MockRepository를 만들어 주는 것이다.
    겉만 같은 가짜 repository인 것이다.

Mockito 수행과정🙄

모의 객체 생성 → 메서드 호출 예상 동작 설정 → 메서드 호출 검증
  1. 모의 객체 생성 : Mock
  • Mockito를 사용하여 테스트에 필요한 객체의 모의(가짜) 객체를 생성
  • 이 모의 객체는 실제 객체와 비슷한 행동을 하지만, 프로그래머가 원하는 대로 조작
  1. 메서드 호출 예상 동작 설정 : Stub
  • 모의 객체의 메서드 호출에 대한 예상 동작을 정의합니다.
  1. 메서드 호출 검증 : Verify
  • 모의 객체에 대해 특정 메서드가 호출되고 예상된 인자와 함께 호출되었는지를 검증하는 메서드를 제공합니다
// 1. 모의 객체 생성 : Mock
List<String> mockList = Mockito.mock(List.class);

// 2. 메서드 호출 예상 동작 설정 : Stub
Mockito.when(mockList.size()).thenReturn(5);

// 3. 메서드 호출 검증 : Verify
Mockito.verify(mockList).add("item");

Mockito 주요 용어😣

용어분류설명
Mock용어실제 객체를 대신하여 프로그래밍을 테스트할 수 있는 모의 객체를 생성하는 것으로 특정 동작이나 결과를 설정하고 검증하기 위해 사용
Stub용어특정 메서드 호출에 대해 미리 정의된 동작을 반환하는 객체로, 테스트에서 사용될 때 실제 동작이 아닌 가짜 동작을 수행
Spy용어실제 객체를 사용하면서 해당 객체의 일부 동작을 감시하고 기록할 수 있는 객체
Mocking용어특정 동작이나 결과를 시뮬레이션하기 위해 모의 객체를 생성하거나 가짜 동작을 정의하는 것
Verification용어메서드 호출이나 객체 동작이 예상대로 수행되었는지 확인하는 작업
Matchers메서드모킹이나 확인 작업에서 사용되는 매개변수 일치 여부를 확인하는 데 사용되는 메서드를 제
MockitoAnnotations클래스Mockito 애너테이션을 사용하여 Mock 및 Spy 객체를 초기화하는 데 사용되는 클래스
MockitoJUnitRunner클래스JUnit 테스트에 Mockito를 사용하는 데 필요한 설정을 자동으로 처리하는 러너 클래스
MockitoJUnitRunner.Silent클래스MockitoJUnitRunner와 유사하지만, Mock 객체를 생성하지 않은 클래스에서도 실행

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를 반환

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);

Spy

  • 기존 객체의 일부 메서드를 원본 동작을 유지하면서 변경하거나 감시할 수 있게 해주는 기능을 제공

  • 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"를 반환

모킹(Mocking)

  • 모의 객체의 생성을 통해 실제 객체의 동작을 모방하는 모의 객체를 만드는 과정을 의미

  • 이러한 모의객체는 테스트 중인 코드를 격리하고 다양한 시나리오를 시뮬레이션하는 데 사용

// 예시 클래스
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)

  • 모의 객체의 메서드 호출을 확인하는 프로세스를 의미

  • 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();

Mockito 어노테이션 & 메서드🍟

Annotation설명
@Mock모의 객체(Mock Object)를 생성하는데 사용됩니다.
@Spy스파이 객체(Spy Object)를 생성하는데 사용됩니다.
@Captor모의 객체에 전달된 메서드 인수를 캡처하는데 사용됩니다.
@InjectMocks테스트 대상이 되는 객체에 ‘모의 객체를 자동으로 주입’할때 사용이 됩니다.
@MockBean스프링 프레임워크에서 사용되며, 테스트용 Mock 객체를 생성하고 주입하는 데 사용됩니다.
@MockitoSettingsMockito의 설정을 변경하거나 커스터마이즈할 때 사용됩니다.
@MockitoJUnitRunnerJUnit 테스트에서 Mockito를 사용하기 위해 실행할 때 사용됩니다.
@BDDMockitoBDD 스타일의 테스트를 위해 Mockito를 사용할 때 사용됩니다.

@InjectMocks, @Mock, @MockBean 차이점

  • @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;
        }
    }
}

Mockito: Given-When-Then 패턴

  1. Mockito의 Given 단계
  • 테스트 시나리오에서 ‘사전 조건’
  • 즉, 객체의 초기 상태나 호출된 메서드의 입력 값을 설정
    예를 들어, Given 단계에서 모의 객체의 특정 메서드가 호출되었을 때 어떤 값을 반환하도록 설정
  1. Mockito의 When 단계
  • 테스트할 메서드를 호출하는 부분

  • 이 부분에서는 실제로 테스트 대상 메서드를 호출하여 원하는 ‘동작을 검증’

  1. Mockito의 Then 단계
  • ‘테스트 결과를 검증’하는 부분

  • 예상되는 결과를 검증하고, 모의 객체의 메서드가 예상대로 호출되었는지를 확인할 수 있다.

Given-When-Then 패턴 예시

  1. Given : 사전 조건
  • mockObject의 someMethod() 메서드가 호출되었을 때 expectedValue를 반환하도록 설정
  1. When : 동작을 검증
  • testedObject의 testedMethod()를 호출하여 실제로 테스트 대상 메서드를 실행
  1. Then : 테스트 결과 검증
  • result가 expectedValue와 일치하는지를 검증하고, mockObject의 someMethod()가 예상대로 호출되었는지를 확인
// Given
Mockito.when(mockObject.someMethod()).thenReturn(expectedValue);

// When
Object result = testedObject.testedMethod();

// Then
assertEquals(expectedValue, result);
Mockito.verify(mockObject).someMethod();

Controller Test를 왜 mock으로 하는 이유?

테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스을 단축하기 위해

ontroller 테스트는 그저 간단하게 client에게 HTTP Status를 잘 응답하고 있는 지를 테스트하기 위함인데 RestAssured와 @SpringbootTest를 이용하는 것은 그다지 좋은 생각은 아니다.

service의 내부 메서드를 알아야 하거나 테스트를 위해 db에 데이터를 save하거나 로그인을 통해 토큰을 받아오는 번거로운 작업을 mocking 함으로써 테스트를 하는 목적에 맞게 테스트 시간을 절약하고 테스트를 짜는 리소스를 단축하는 것이 맞다는 판단 하에 controller를 mockito를 이용해 테스트하고 있다.

트러블 슈팅💣

1. doThrow().when()을 이용해 예외를 유발시킬 때 발생한 에러

doThrow(NotFoundCrewException.class).when(reservationService)
                .findCrewHistoryByCoach(anyLong());

위와 같이 NotFoundCrewException.class 로 하면 예외 상황에서 HTTP 상태 코드는 잘 오지만 응답 Body값의 칼럼 정보인 message가 null로 반환된다.

doThrow(new NotFoundCrewException()).when(reservationService)
                .findCrewHistoryByCoach(anyLong());

new NotFoundCrewException() 로 하면 message 값 확인이 가능하다.

2. 사용 메서드의 실행 흐름에 검증하는 로직이 있다면 이를 mocking처리 해줘야 한다.

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는 무엇일까??

BDDMockito

Behavior-Driven Development의 약자로 행위 주도 개발을 말한다. 테스트 대상의 상태의 변화를 테스트하는 것이고, 시나리오를 기반으로 테스트하는 패턴을 권장한다.

여기서 권장하는 기본 패턴은 Given, When, Then 구조를 가진다.

@Test
void test() {
	//given
    
    //when
    
    //then

}

Authentication 객체 생성

현재 SpringSecurity를 이용하여 Jwt를 받아 인증과 권한을 확인하고 SecurityContext에 인증/권한 정보를 설정한다. 따라서 테스트에서 인증 과정을 생략하려면 Authentication 객체를 직접 생성해야한다.

나는 TestingAuthenticationToken 라는 테스트 용도로 만들어진 Authentication 구현체를 사용하였다. 이는 사용자 ID, 비밀번호, 권한 등을 직접 지정할 수 있다.

Authentication authentication = new TestingAuthenticationToken("test1@gmail.com", null, "ROLE_USER");

MockMvc 요청에 Authentication 객체 추가하기

생성한 Authentication 객체를 MockMvc 요청에 추가하려면 SecurityMockMvcRequestPostProcessorsauthentication() 메서드를 사용하면 된다.

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을 왜 사용하고 있었지?

profile
Velog에 기록 중

0개의 댓글