Mock

사한·2024년 1월 9일
2
post-thumbnail

안녕하세요~ 사한입니다! 😁
오늘은 소프트웨어 테스트에 있어서 중요한 역할을 하는 'Mock'에 대해 이야기해보려 합니다.

테스트에서...

프로그래밍에서 테스트는 필수적인 단계입니다.

특히, 소프트웨어의 안정성을 확보하고, 예상치 못한 버그를 미리 발견할 수 있는 테스트는 매우 중요한 과정입니다.

하지만, 실제 환경에서의 동작을 그대로 테스트하기에는 다양한 한계가 있습니다.

예를 들어, 데이터베이스나 외부 서비스에 의존하는 코드의 경우, 실제 환경에서의 동작을 그대로 테스트하면 테스트가 복잡해지고, 실행 시간이 길어지며, 결과도 일관성이 떨어질 수 있습니다.

또는 여러 요소와 결합되어 있어서 테스트가 어려운 경우도 있을 것 같습니다.

이러한 문제를 해결하기 위해 등장한 것이 바로 'Mock'입니다.

Mock 이란?

Mock은 테스트할 때 실제 객체를 사용하는 대신 가짜 객체를 만들어 사용하는 기법입니다.

Mock은 테스트 더블의 Mock을 의미하며 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체입니다.

테스트 더블( Test Double ) 이란?

xUnit Test Patterns의 저자인 제라드 메스자로스(Gerard Meszaros)가 만든 용어로 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 말합니다.

테스트하려는 객체와 연관된 객체를 사용하기가 어렵고 모호할 때 대신해 줄 수 있는 객체를 테스트 더블이라 합니다.

테스트 더블은 크게 Dummy, Fake, Stub, Spy, Mock으로 나누는데 오늘 포스팅은 Mock에 대한 내용입니다. 😁

해당 사진은 실제 시스템에서 각각의 요소들이 얽혀있는 상황을 퍼즐처럼 표현했습니다.

저 상황에서 주황색 요소를 테스트를 하고 싶습니다.

그러면 주황색과 연결된 파란색 퍼즐들도 필요할테고 파란색 퍼즐도 각각의 초록색이나 노란색 퍼즐들이 필요하게 됩니다.

이를 어쩌죠?

이때 Mock 기법을 사용해 봅시다.

실제 객체가 아닌, 가짜 객체를 만들어서 주황색 요소에 붙였는데요.

가짜 객체는 우리가 설정한 값만을 반환하게 설정하였습니다.

이를 통해 테스트의 복잡성을 줄이고, 일관된 테스트 결과를 얻을 수 있게 되었습니다.

또한 Mock을 통해 테스트의 범위를 제한하고 의존성을 관리할 수 있어, 테스트 코드의 품질도 높일 수 있습니다.

한가지 더 예시를 들어보겠습니다!

현재 날씨를 가져오는 외부 API를 사용중이고 날씨를 가져온뒤 특정온도에 맞는 출력값을 반환하고 있는 상황입니다.

public class WeatherService {
    private HttpClient httpClient;

    public WeatherService(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public String getCurrentWeather(String location) {
        String response = httpClient.get("http://api.weather.com/" + location);
        return parseWeather(response);
    }

    private String parseWeather(String response) {
        // 날씨 데이터 파싱 코드
        int temperature = ...; // API에서 받아온 온도 정보
        return getWeatherComment(temperature);
    }

    private String getWeatherComment(int temperature) {
        if (temperature <= 0) {
            return "너무 추워요";
        } else if (temperature <= 10) {
            return "쌀쌀해요";
        } else if (temperature <= 20) {
            return "포근해요";
        } else if (temperature <= 30) {
            return "더워요";
        } else {
            return "너무 더워요";
        }
    }
}

코드를 살펴보면 0도이하면 "너무 추워요", 1~10도면 "쌀쌀해요" 등으로 값을 출력하고 있습니다.

이때 모든 case를 테스트로 진행하려면 어떻게 해야할까요?

날씨의 온도는 외부 API이기 때문에 저희가 제어할 수 없는 영역이죠?

이와 같이 외부 API 처럼 제어할 수 없는 영역을 대체할때도 mock을 사용하게 됩니다.

자바에서 Mock 기본 사용법

자바에서는 여러 Mock 프레임워크를 사용할 수 있지만, 가장 널리 사용되는 것은 Mockito라는 라이브러리입니다.

Mockito는 간편하면서도 강력한 기능을 제공하여 Mock 객체를 쉽게 만들고 관리할 수 있게 해줍니다.

예를 들어, 다음 코드는 'SomeClass'라는 클래스의 인스턴스를 'Mock'으로 생성하는 예제입니다.

SomeClass someClassMock = Mockito.mock(SomeClass.class);

이렇게 생성된 Mock 객체는 기본적으로 모든 메서드 호출에 대해 'null'을 반환합니다.

하지만 Mockito의 'when' 함수를 사용하면, 특정 메서드 호출에 대한 반환 값을 설정할 수 있습니다.

예를 들어, 다음 코드는 'someMethod' 메서드가 호출될 때 'someValue'를 반환하도록 설정하는 예제입니다.

Mockito.when(someClassMock.someMethod()).thenReturn(someValue);

그리고 Mockito의 'verify' 함수를 사용하면, Mock 객체에 대한 특정 메서드 호출이 예상대로 이루어졌는지 검증할 수 있습니다.

Mockito.verify(someClassMock).someMethod();

Mockito 사용법

mock: mock 메서드는 특정 클래스에 대한 가짜 객체를 생성합니다.

List mockedList = Mockito.mock(List.class);

when: when 메서드는 가짜 객체의 특정 메서드가 호출되었을 때 어떤 동작을 해야 하는지 정의합니다.

Mockito.when(mockedList.size()).thenReturn(100);

Argument Matchers: Mock 할 메서드가 내부에서 어떤 인수로 실행될 지 모를 때가 있습니다.

예를 들면 스터디를 생성하는 메서드를 테스트를 해야 할때를 코드로 살펴봅시다.

when(studyRepository.findById(any())).thenReturn(Optional.of(study));

any()를 사용하면 어떤 인수로 메서드가 실행되든 동일한 결과를 반환합니다. 이외에도 다양한 인터페이스가 존재합니다.

  • anyInt(), anyBoolean(), anyFloat(), anyString() ...
  • anySet(), anyMap(), anyIterable() ...
  • isNull(), isNotNull() ...
  • contains(), startWith(), artThat() ...

Throw: thenThrow()를 사용하면 Mock 한 객체가 특정 메서드 실행에서 예외를 던지도록 할 수 있습니다.

Mockito.when(mockedList.get(Mockito.anyInt())).thenThrow(new IllegalArgumentException());

void 타입의 경우 doThrow() 메서드를 사용해야 합니다.

doThrow(new IllegalArgumentException()).when(studyRepository).validate(1L);

verify: verify 메서드는 가짜 객체의 특정 메서드가 기대한 대로 호출되었는지 검증합니다.

veriry의 인수에 times(N)를 넘겨서 studyRepository.findById(1L)이 정확히 N번 호출되지 않았으면 예외가 발생합니다.

verify(studyRepository, times(1)).findById(1L);

never()을 인수로 넘기면 해당 함수가 한 번이라도 실행되면 예외가 발생합니다.

verify(studyRepository, never()).findById(1L);

추가로 자주 사용하는 아래와 같은 기법들이 있습니다.

  • atMostOnce()
  • atLeastOnce()
  • atLeast(N)
  • atMost(N)

InOrder를 사용해서 순서를 검증할 수도 있습니다.

InOrder inOrder = inOrder(studyRepository);
inOrder.verify(studyRepository).create(study);
inOrder.verify(studyRepository).findById(1L);

이 경우 studyRepository.create(study)가 studyRepository.findById(1L)보다 먼저 실행되지 않았다면 예외가 발생합니다.

정리

Mock은 테스트 작성 시 매우 중요한 도구입니다.

복잡한 의존성을 가진 코드를 테스트하기 위해 실제 객체를 사용하는 것은 실행 시간을 늘리고, 테스트의 복잡성을 증가시키며, 일관된 테스트 결과를 얻기 어렵게 만듭니다.

이런 문제를 해결하기 위해, Mock을 사용하여 테스트의 범위를 제한하고, 의존성을 관리하며, 테스트 코드의 가독성을 높일 수 있습니다.

자바에서는 Mockito와 같은 Mock 프레임워크를 사용하여 쉽게 Mock 객체를 생성하고 관리할 수 있습니다.

이를 통해 테스트가 더욱 간편해지고, 테스트 코드의 품질도 높아집니다.

하지만 모든 상황에서 Mock을 사용하기 보단 테스트 할 대상이 무엇인지 정확하게 파악하고 충분히 고민한다면 더 좋은 설계와 테스트 코드를 작성할 수 있을 것 같습니다! 😁

참고

https://tech.kakao.com/2021/09/29/mocking-fe/

https://www.crocus.co.kr/1555

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html

https://jaehoney.tistory.com/219

https://velog.io/@jmjmjmz732002/%EC%99%B8%EB%B6%80-API-%EC%84%9C%EB%B2%84%EB%8A%94-mocking%ED%95%98%EC%97%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4

https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/

https://tecoble.techcourse.co.kr/post/2020-10-16-is-ok-mockito/

profile
요행을 바라지 말자

0개의 댓글