Spring and Spring Boot/Class, Annotation, Library

Library : Mockito에서 제공하는 when ~ then 구문을 활용한 스프링부트 단위 테스트

마이트너 2025. 3. 4. 13:54

when ~ then을 사용한 검증

Mockito는 Java에서 가장 인기 있는 Mocking 라이브러리로, 주로 단위 테스트에서 객체 간의 의존성을 Mock 처리하고, 이를 통해 독립적인 테스트 환경을 구축할 수 있도록 돕습니다. Mockito에서 테스트할 때, when ~ then 구문은 동작 설정 및 검증을 위한 핵심적인 부분입니다.

when(mockObject.method()).thenReturn(someValue);

when(mockObject.method())

Mockito 라이브러리에서 Mock 객체의 메서드를 호출하는 형태입니다. 이 구문을 사용할 때, Mock 객체는 실제 구현이 아닌, 모킹(mocking)된 객체입니다. 이 객체는 가짜 객체로, 실제 구현체가 아닌 테스트에서 예상한 동작을 하도록 설정할 수 있습니다. 이때 메서드는 실제로 실행되지 않고, Mockito의 설정에 따라 가짜 동작을 반환하게 됩니다.

 

  • 예시 : when() 구문은 Mockito에서 Mock 객체의 메서드 호출에 대한 응답을 설정할 때 사용됩니다. 이 구문을 사용하여 Mock 객체의 메서드가 호출될 때 어떤 결과를 반환할지 미리 정의할 수 있습니다.
when(mockObject.method()).thenReturn(someValue);

 

  • 예시 : when(mockObject.method()) 구문을 사용하면, 해당 메서드 실제 코드를 실행하지 않습니다. 대신 Mockito가 설정한 동작을 반환하게 됩니다. 예를 들어, 아래와 같은 코드에서 mockObject.method()는 실제 메서드를 호출하지 않으며, thenReturn에 설정한 값만 반환합니다. 즉, when(mockObject.method())는 메서드 호출을 감지하는 부분이며, 실제로 메서드가 실행되는 것이 아니라 Mock 객체에 설정된 동작을 실행하게 되는 것입니다. 이 메커니즘을 통해 외부 시스템에 의존하지 않고, 테스트를 독립적으로 수행할 수 있습니다.
public class MyService {
    private MyRepository myRepository;

    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }

    public String getData() {
        return myRepository.getData();  // 실제 구현이 아닌 Mock 객체의 메서드 호출
    }
}

public class MyServiceTest {
    @Test
    public void testGetData() {
        // Mock 객체 생성
        MyRepository myRepository = mock(MyRepository.class);

        // mockObject.getData()가 호출되면 "Mocked Data"를 반환하도록 설정
        when(myRepository.getData()).thenReturn("Mocked Data");

        // MyService를 생성하여 테스트
        MyService myService = new MyService(myRepository);
        
        // getData() 메서드를 호출하면 실제 구현이 아닌 Mocked 값을 반환
        String result = myService.getData();

        // 결과 검증
        assertEquals("Mocked Data", result);
    }
}

thenReturn(value)

메서드 호출이 이루어지면 지정된 값을 반환하도록 설정합니다.

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class MyServiceTest {

    @Test
    public void testServiceMethod() {
        // 1. Mock 객체 생성
        MyRepository mockRepository = mock(MyRepository.class);
        
        // 2. when ~ then 구문을 사용해 동작 설정
        when(mockRepository.getData()).thenReturn("Mocked Data");

        // 3. 실제 서비스 객체 생성
        MyService myService = new MyService(mockRepository);

        // 4. 서비스 메서드 호출 및 검증
        String result = myService.getDataFromRepository();
        assertEquals("Mocked Data", result);
    }
}
예시 설명

when(mockRepository.getData()).thenReturn("Mocked Data");는 getData() 메서드가 호출되었을 때 "Mocked Data"를 반환하도록 설정합니다.

MyService의 getDataFromRepository() 메서드를 호출하면, mockRepository.getData()는 실제 데이터베이스나 외부 서비스와 연결된 게 아니라 Mock 객체에서 설정한 "Mocked Data"를 반환합니다.

thenAnswer(value)

Mockito에서 thenAnswer는 특정 메서드 호출에 대해 맞춤형 동작을 정의하고 싶을 때 사용하는 메서드입니다. thenAnswer를 사용하면, 메서드 호출에 대해 동적이고 복잡한 동작을 정의할 수 있습니다. 이는 thenReturn과 비슷하지만, thenReturn은 고정된 값을 반환하는 반면, thenAnswer는 좀 더 복잡한 동작을 할 수 있게 해줍니다. thenAnswer는 Answer 인터페이스를 구현하여 동작을 정의합니다. 이 방식은 주로 입력 인자에 따라 다르게 동작하거나 동적 계산을 통해 값을 반환하는 경우에 유용합니다. 

when(mock.method(any())).thenAnswer(invocation -> {
    // invocation은 메서드 호출에 대한 정보를 담고 있습니다.
    // 필요한 동작을 정의하고, 그 결과를 반환합니다.
    return someResult;
});

 

 

1. thenAnswer의 구성요소

  • invocation 객체 : thenAnswer에서 전달되는 invocation 객체는 호출된 메서드에 대한 정보를 담고 있습니다. 이를 통해 호출된 메서드의 인자나 메서드 이름을 조회할 수 있습니다.
  • Answer 인터페이스 : 동적으로 반환값을 생성할 수 있게 해줍니다.
//메서드 이름과 인자 조회

when(myRepository.getData(anyInt())).thenAnswer(invocation -> {
    // 메서드 이름을 가져옵니다.
    String methodName = invocation.getMethod().getName();

    // 첫 번째 인자값을 가져옵니다.
    Integer argument = invocation.getArgument(0);

    return "Method: " + methodName + " with argument: " + argument;
});
예시 설명 : invocation.getMethod().getName()을 통해 호출된 메서드의 이름을 얻을 수 있고, invocation.getArgument(0)을 통해 인자 값을 가져올 수 있습니다.

 

 

2. thenAnswer의 사용

thenAnswer는 보통 복잡한 반환값을 생성하거나, 동적인 동작을 구현할 때 사용됩니다. thenAnswer는 동적인 동작을 정의할 때 매우 유용한 기능입니다. when ~ thenReturn을 사용하여 단순히 값을 반환하는 것보다 더 복잡한 동작을 필요로 할 때, thenAnswer를 통해 입력 값에 따른 동작, 동적 계산, 또는 예외 처리 등을 구현할 수 있습니다. 이는 테스트의 유연성을 높여 주며, 특정 메서드 호출에 대해 보다 정교한 동작을 설정할 수 있게 해줍니다. 예를 들어 아래와 같을 때가 있습니다.

  • 동적 계산 : 메서드의 인자에 따라 계산된 값을 반환하는 경우
  • 조건부 반환 : 특정 조건에 따라 다른 값을 반환하거나 예외를 던지는 경우
  • 외부 시스템 동작 모방 : 외부 시스템을 호출하는 메서드에서, 호출 결과가 동적으로 변하는 경우

 

2-(1) 단순한 동적 반환 값 

  • thenAnswer는 getData(anyInt()) 메서드 호출 시, 인자로 전달된 값을 기반으로 동적으로 값을 반환합니다.
  • invocation.getArgument(0)을 사용하여 첫 번째 인자 값을 가져오고, 이 값을 이용해 동적으로 결과를 반환합니다. myRepository.getData(1)이 호출되면 "Data for ID 1"을 반환하게 됩니다.
//단순한 동적 반환 값

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class MyServiceTest {

    @Test
    public void testDynamicAnswer() {
        // Mock 객체 생성
        MyRepository myRepository = mock(MyRepository.class);

        // when ~ thenAnswer를 사용하여 동적 반환 값 설정
        when(myRepository.getData(anyInt())).thenAnswer(invocation -> {
            Integer argument = invocation.getArgument(0);  // 첫 번째 인자
            return "Data for ID " + argument;
        });

        // 메서드 호출
        String result = myRepository.getData(1);
        
        // 결과 검증
        assertEquals("Data for ID 1", result);
    }
}

 

 

2-(2) 입력 값에 따라 동작을 다르게 설정 

  • thenAnswer를 사용하여 입력값에 따라 다르게 동작하는 로직을 정의할 수 있습니다.
  • invocation.getArgument(0)을 통해 메서드의 첫 번째 인자값을 가져오고, 이를 기반으로 다른 값을 반환하도록 설정합니다.
  • 예를 들어, getData(1)을 호출하면 "Data for ID 1", getData(2)를 호출하면 "Data for ID 2"가 반환됩니다.
//입력 값에 따라 동작을 다르게 설정

@Test
public void testDynamicBehaviorBasedOnInput() {
    // Mock 객체 생성
    MyRepository myRepository = mock(MyRepository.class);

    // when ~ thenAnswer로 입력 값에 따라 동적으로 반환 값 설정
    when(myRepository.getData(anyInt())).thenAnswer(invocation -> {
        Integer argument = invocation.getArgument(0);
        if (argument == 1) {
            return "Data for ID 1";
        } else if (argument == 2) {
            return "Data for ID 2";
        }
        return "Unknown Data";
    });

    // 메서드 호출 및 결과 검증
    assertEquals("Data for ID 1", myRepository.getData(1));
    assertEquals("Data for ID 2", myRepository.getData(2));
    assertEquals("Unknown Data", myRepository.getData(3));
}

 

 

2-(3) 예외를 던지기

  • thenAnswer는 예외를 던지는 동작을 정의할 수도 있습니다.
  • 예를 들어, 특정 조건에 맞을 때 예외를 발생시키는 경우를 다룰 수 있습니다.
  • invocation.getMethod().getName()을 통해 호출된 메서드의 이름을 얻을 수 있고, invocation.getArgument(0)을 통해 인자 값을 가져올 수 있습니다.
//예외를 던지기

@Test
public void testThrowingException() {
    // Mock 객체 생성
    MyRepository myRepository = mock(MyRepository.class);

    // when ~ thenAnswer로 예외 던지기
    when(myRepository.getData(anyInt())).thenAnswer(invocation -> {
        Integer argument = invocation.getArgument(0);
        if (argument == 0) {
            throw new IllegalArgumentException("Invalid ID");
        }
        return "Data for ID " + argument;
    });

    // 메서드 호출 및 예외 검증
    assertThrows(IllegalArgumentException.class, () -> myRepository.getData(0));
    assertEquals("Data for ID 1", myRepository.getData(1));
}

thenThrow(value)

 


when ~ then의 다양한 사용 예

 

1. 여러 번 호출에 대해 다른 값을 반환 : thenReturn은 여러 번 호출될 경우 각 호출에 대해 서로 다른 값을 반환할 수 있습니다. 첫 번째 호출에서는 "First Call"을 반환하고, 두 번째 호출에서는 "Second Call"을 반환합니다.

//여러 번 호출에 대해 다른 값을 반환

when(mockRepository.getData())
    .thenReturn("First Call")
    .thenReturn("Second Call");

System.out.println(mockRepository.getData());  // "First Call"
System.out.println(mockRepository.getData());  // "Second Call"

 

 

2. 예외를 던지기 : thenThrow를 사용하여 특정 메서드 호출 시 예외를 던질 수도 있습니다. getData() 메서드를 호출하면 RuntimeException 예외가 발생합니다.

//예외를 던지기

when(mockRepository.getData())
	.thenThrow(new RuntimeException("Data not found"));

assertThrows(RuntimeException.class, () -> mockRepository.getData());

 

 

3. 특정 인수에 대해 동작 설정 : 메서드의 매개변수에 따라 다르게 동작하게 만들 수도 있습니다. getDataById(1)이 호출되면 "Data for ID 1"을 반환하고, getDataById(2)가 호출되면 "Data for ID 2"를 반환합니다.

when(mockRepository.getDataById(1)).thenReturn("Data for ID 1");
when(mockRepository.getDataById(2)).thenReturn("Data for ID 2");

System.out.println(mockRepository.getDataById(1));  // "Data for ID 1"
System.out.println(mockRepository.getDataById(2));  // "Data for ID 2"

when ~ then을 사용한 검증

Mockito는 Mock 객체의 동작뿐만 아니라 메서드 호출이 실제로 이루어졌는지 검증할 수 있는 기능도 제공합니다. 이를 통해 우리가 예상한 대로 Mock 객체가 호출되었는지 확인할 수 있습니다. 

 

1. 메서드 호출 여부 검증

verify(mockRepository).getData();는 getData() 메서드가 적어도 한 번 호출되었는지 확인합니다.

//메서드 호출 여부 검증

MyRepository mockRepository = mock(MyRepository.class);
MyService myService = new MyService(mockRepository);

myService.getDataFromRepository();

// verify로 호출 여부 검증
verify(mockRepository).getData();  // getData() 메서드가 호출되었는지 확인

 

 

2. 호출 횟수 검증

times(1)은 getData() 메서드가 한 번만 호출되었는지를 검사합니다.

//호출 횟수 검증

verify(mockRepository, times(1)).getData();  // getData()가 정확히 한 번 호출되었는지 확인

when ~ then의 활용 예시 : 외부 API 호출을 Mocking

  • weatherApi.fetchWeather("Seoul")는 실제 API를 호출하는 것이 아니라, Mock 객체에서 설정한 "Sunny" 값을 반환합니다.
  • 이와 같은 방식으로 외부 서비스나 API의 호출을 Mock하여, 의존성을 격리하고 유닛 테스트를 수행할 수 있습니다.
public class WeatherService {
    private final WeatherApi weatherApi;

    public WeatherService(WeatherApi weatherApi) {
        this.weatherApi = weatherApi;
    }

    public String getWeather(String city) {
        return weatherApi.fetchWeather(city);
    }
}

// 테스트 코드
@ExtendWith(MockitoExtension.class)
public class WeatherServiceTest {

    @Mock
    private WeatherApi weatherApi;

    @InjectMocks
    private WeatherService weatherService;

    @Test
    void testGetWeather() {
        // 외부 API 호출을 Mocking
        when(weatherApi.fetchWeather("Seoul")).thenReturn("Sunny");

        // 서비스 메서드 호출 및 결과 확인
        String result = weatherService.getWeather("Seoul");
        assertEquals("Sunny", result);
    }
}

728x90