Logo

[파이썬] 테스트 모킹 - unittest.mock

본 포스팅의 내용을 문제없이 이해하려면 파이썬의 기본 테스팅에 대한 선수 지식이 필요합니다. 해당 지식이 부족하신 분들은 관련 포스팅를 먼저 읽고 돌아오시는 것을 추천드립니다.

Mocking이란?

단위 테스트를 작성하다보면 데이터베이스 또는 외부 API에 의존하는 코드를 테스트해야 할 일이 필연적으로 생기기 마련입니다. 운영 환경 대비 제약이 많은 테스트 환경에서는 실제 데이터베이스와 연동하거나 실제 외부 API를 호출하기가 불가능한 경우가 많습니다. 가령 가능하더라도, 이렇게 외부 서비스에 의존하는 테스트는 해당 서비스에 문제가 있을 경우 깨질 수 있으며 실행 속도도 느릴 수 밖에 없습니다.

따라서 단위 테스트를 작성할 때 외부에 의존하는 부분을 임의의 가짜로 대체하는 기법이 자주 사용되는데 이를 모킹(mocking)이라고 합니다. 다시 말해, 모킹(mocking)은 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 단위 테스트를 작성하기 위해서 사용되는 테스팅 기법입니다.

unittest.mock 모듈

unittest.mock 모듈은 파이썬 3.3부터 언어 자체에 기본 내장되어 있는 모킹 라이브러리입니다. 따라서 별도의 외부 라이브러리 설치없이 파이썬 인터프리터에서 다음과 같이 임포트해서 바로 사용을해 볼 수 있습니다. 이 모듈을 이용하면 단위 테스트를 작성할 때 코드의 특정 부분을 mock 객체로 대체할 수 있으며, 해당 mock 객체가 어떻게 사용되었는 검증할 수 있습니다.

>>> from unittest.mock import Mock, MagicMock, call

Mock 객체 설정하기

mocking은 소외 mock이라고 불리는 가짜 객체를 생성하는 것부터 시작합니다. 우리는 이 mock 객체가 어떻게 작동을 할지를 지정해줄 수 있으며, 이 mock 객체는 자신을 상대로 어떤 작업이 일어났는지를 기억합니다.

먼저 호출되었을 때 특정 값을 리턴하는 mock 객체는 return_value 옵션을 이용해서 생성할 수 있습니다.

>>> from unittest.mock import Mock
>>> mock = Mock(return_value='Hello, Mock!')
>>> mock()
'Hello, Mock!'

반면에 호출되었을 때 예외가 발생하는 mock 객체는 side_effect 옵션을 이용해서 생성할 수 있습니다.

>>> mock = Mock(side_effect=Exception('Oops!'))
>>> mock()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 1011, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 1071, in _mock_call
    raise effect
Exception: Oops!

side_effect 옵션에 리스트를 넘기면 mock 객체가 호출될 때 마다 매 번 다른 값을 리턴할 수도 있습니다.

>>> mock = Mock(side_effect=[1, 2, 3])
>>> mock()
1
>>> mock()
2
>>> mock()
3
>>> mock()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 1011, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 1073, in _mock_call
    result = next(effect)
StopIteration

side_effect 옵션에 함수를 넘기면 mock 객체를 호출했을 때 주어진 인자에 따라 다른 값을 리턴할 수 있습니다.

>>> mock = Mock(side_effect=lambda x: x * 10)
>>> mock(1)
10
>>> mock(2)
20

return_valueside_effect 옵션은 꼭 Mock() 생성자의 인자로 넘어갈 필요는 없습니다. 다음과 같이 mock 생성 이후에도 얼마든지 이 옵션 값은 바꿀 수가 있습니다.

>>> mock = Mock()
>>> mock.return_value = 1
>>> mock()
1
>>> mock.return_value = 2
>>> mock()
2

이 mock이라는 녀석은 위와 같이 함수처럼 바로 호출을 할 수도 있지만, 객체처럼 속성도 가질 수 있는데 각 속성은 새로운 mock이 됩니다. 따라서 다음과 같이 특정 속성에 값을 할당해 수도 있고, 특정 메서드의 리턴 값을 지정해줄 수도 있습니다.

>>> mock = Mock()
>>> mock.attribute = 'ATTRIBUTE'
>>> mock.attribute
'ATTRIBUTE'
>>> mock.method.return_value = 'METHOD RETURN VALUE'
>>> mock.method()
'METHOD RETURN VALUE'

이렇게 mock 객체의 속성이나 메서드도 또 다른 mock 객체가 된다는 점을 잘 활용하면 매우 유연한 mocking이 가능해집니다.

Mock 객체 검증 하기

mock 객체는 자신에게 발생했던 작업들을 검증할 수 있도록 다양한 메서드를 제공하고 있습니다. 대표적으로, assert_called() 메서드는 해당 mock이 호출된 이력이 있는지를 검증할 때 쓰입니다.

예를 들어, mock을 한 번도 호출하지 않고 assert_called() 메서드를 호출하면 예외가 발생합니다.

>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.assert_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 845, in assert_called
    raise AssertionError(msg)
AssertionError: Expected 'None' to have been called.

하지만 mock 객체를 한 번 호출한 후에 다시 assert_called() 메서드를 호출하면 예와가 발생하지 않습니다.

>>> mock()
<Mock name='mock()' id='4559483664'>
>>> mock.assert_called()

assert_called_once() 메서드는 해당 mock이 단 한 번 호출되었는지 검증할 때 쓰입니다.

>>> mock = Mock()
>>> mock()
<Mock name='mock()' id='4559483280'>
>>> mock.assert_called_once()
>>> mock()
<Mock name='mock()' id='4559483280'>
>>> mock.assert_called_once()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 854, in assert_called_once
    raise AssertionError(msg)
AssertionError: Expected 'mock' to have been called once. Called 2 times.

assert_called_with() 메서드를 사용하면 해당 mock이 호출되었을 때 어떤 인자가 넘어왔는지까지도 검증할 수 있습니다.

>>> mock = Mock()
>>> mock('A', B='C')
<Mock name='mock()' id='4559461968'>
>>> mock.assert_called_with('A', B='C')

assert_not_called() 메서드는 지금까지와 반대로 해당 mock이 호출된 적이 없는지 검증할 때 쓰입니다.

>>> mock = Mock()
>>> mock.assert_not_called()
>>> mock()
<Mock name='mock()' id='4559461712'>
>>> mock.assert_not_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/dale/.pyenv/versions/3.7.6/lib/python3.7/unittest/mock.py", line 836, in assert_not_called
    raise AssertionError(msg)
AssertionError: Expected 'mock' to not have been called. Called 1 times.

mock 객체는 다양한 검증 메서드 뿐만 아니라 몇가지 유용히 속성들도 제공하고 있습니다.

call_count는 해당 mock이 호출된 횟수를 기억하고 있습니다.

>>> mock = Mock(return_value=None)
>>> mock()
>>> mock()
>>> mock.call_count
2

call_args는 해당 mock이 마지막 호출되었을 때 넘어온 인자를 기억하고 있습니다.

>>> mock = Mock(return_value=None)
>>> mock(1, 2, c=3)
>>> mock.call_args
call(1, 2, c=3)

MagicMock

파이썬에는 매직 메서드(magic method)라는 개념이 있는데, 모든 객체에는 언어 레벨에서 특수한 목적으로 쓰이는 메서드들을 정의할 수 있습니다. 대표적으로 __str__의 경우, 객체를 읽기 좋은 형태의 문자열로 출력하기 위해서 사용되는 매직 메서드입니다.

기본적으로 Mock 클래스를 사용하면 이러한 매직 메서드가 자동으로 모킹되지 않습니다.

>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock.__str__.return_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'method-wrapper' object has no attribute 'return_value'

따라서, 매직 메서드를 모킹하려면 다른 속성이나 메서드와 달리 다음과 같이 새로운 mock 객체를 직접 생성해서 할당을 해줘야 하는 불편함이 있습니다.

>>> mock.__str__ = Mock(return_value = "I'm a mock.")
>>> str(mock)
"I'm a mock."

하지만 Mock 클래스의 확장 버전인 MagicMock 클래스를 사용하면 이러한 매직 메서드를 미리 알아서 모킹을 해놓기 때문에 편리합니다.

>>> from unittest.mock import MagicMock
>>> mock = MagicMock()
>>> mock.__str__.return_value
"<MagicMock id='4556752144'>"
>>> mock.__str__.return_value = "I'm a magic mock."
>>> str(mock)
"I'm a magic mock."

마치면서

지금까지 unittest.mock 모듈의 Mock 또는 MagicMock 클래스를 사용하여 mocking하는 방법에 대해서 알아보았습니다. 실제 프로젝트에서 단위 테스트를 작성할 때는 이렇게 직접 mock을 생성하는 것 보다는 patch() 데코레이터를 사용하는 것이 더 일반적입니다. 이 부분에 대해서는 추후 포스팅를 통해 다뤄보도록 하겠습니다.