[Python] Mock 객체란? Mock 객체를 이용한 제어된 테스트하기
진지한 파이썬 를 읽고, 정리한 글입니다.
목차
· Mock 객체란?
· mock 라이브러리 사용법
Mock 객체란?
· Mock 객체는 실제 애플리케이션 객체의 동작을 모방하는 시뮬레이션된 객체이며, 특별히 제한된 방식으로 모방한다.
· Mock 객체는 코드를 테스트하려는 조건을 정확하게 설명하는 환경을 만드는 데 유용하다.
· 객체의 동작을 독립시키고 코드를 테스트하기 위한 모든 객체를 Mock 객체로 바꿀 수 있다.
ex) HTTP 서버를 직접 생성하지 않고, HTTP 클라이언트를 작성하여 값을 반환하는 다양한 시나리오를 테스트한다.
· 파이썬에서 모의 객체를 만들기 위한 표준 라이브러리는 mock으로, 파이썬 3.3부터 모의 객체가 파이썬 표준 라이브러리에 unittest.mock으로 병합되었다.
- 다음 코드로 파이썬 3.3과 이전 버전의 호환성을 유지할 수 있다.
try:
from unittest import mock
except ImportError:
import mock
mock 라이브러리 사용법
· mock 객체에 속성으로 접근하는 방식으로 사용한다. 모든 값을 속성으로 설정할 수 있다.
· mock 객체는 런타임에 동적으로 만들어진다.
· 예시1 - mock.Mock 속성을 사용해서 접근하기
try:
from unittest import mock
except ImportError:
import mock
m = mock.Mock()
m.some_attribute = "hello world"
print(m.some_attribute)
· 예시2 - 무엇이든 인수로 받아들이고 항상 42를 반환하는 가짜 메서드 만들기
try:
from unittest import mock
except ImportError:
import mock
m = mock.Mock()
m.some_method.return_value = 42
print("m.some_method():", m.some_method())
print("m.some_method(\"with\",\"arguments\"):", m.some_method("with", "arguments"))
· 예시3 - 부수 효과가 있는 mock.Mock 객체에 메서드 생성하기
- 동적으로 생성된 메서드는 의도적인 side effect를 가질 수 있다.
- 즉, 값을 반환하는 상용구 메서드가 아니라 유용한 코드를 실행하도록 정의할 수 있다.
# 부수 효과가 있는 mock.Mock 객체에 메서드 생성하기
try:
from unittest import mock
except ImportError:
import mock
m = mock.Mock()
def print_hello():
print("hello world!")
return 43
m.some_method.side_effect = print_hello # 1
print("m.some_method():")
print(m.some_method())
print()
print("m.some_method.call_count:", m.some_method.call_count) # 2
- 1# 처럼 Mock 객체의 속성에 전체 함수를 할당할 수 있다. 이 방법을 사용하면 Mock 객체 테스트에 필요한 코드를 연결할 수 있어 테스트에서 더 복잡한 시나리오를 구현할 수 있다.
- 2# 처럼 call_count 속성으로 메서드가 호출된 횟수를 확인할 수 있다.
· 예시4 - assert() 메서드 사용하기
- mock 라이브러리는 action -> assertion 패턴을 기반으로 한다.
- 즉, 테스트가 실행되면 Mock 작업이 올바르게 실행되었는지 확인해야한다.
- 이러한 검사를 수행하기 위해 Mock 객체에 assert() 메서드를 적용한다.
https://docs.python.org/ko/3.8/library/unittest.mock.html
try:
from unittest import mock
except ImportError:
import mock
m = mock.Mock()
m.some_method('Ted', 'Robin') # 1
m.some_method.assert_called_once_with('Ted', 'Robin') # 2
m.some_method.assert_called_once_with('Ted', mock.ANY) # 3
m.some_method.assert_called_once_with('Ted', 'Barney')
- assert_called_once_with(), assert_called() 메서드로 Mock 객체에 대한 호출을 확인할 수 있다.
- assert_called_once_with()에는 호출자가 사용할 것으로 예상되는 값을 전달한다. 전달된 값이 사용 중인 값이 아니면 Mock 객체는 AssertionError를 발생한다.
ex) 위 코드를 실행하면, 다음과 같이 오류가 발생한다.
- 전달할 인수를 모르면 #3과 같이 mock.ANY 값을 입력할 수 있다. 이렇게 하면 Mock 메서드에 전달된 인수는 모두 매칭된 다.
· 예시5 - mock.patch 사용하기
- mock 라이브러리는 외부 모듈의 일부 기능, 메서드 또는 객체를 바꿀 때 사용할 수 있다.
- 다음 예시에서 os.unlink() 함수를 가짜 함수로 바꾸어보자.
import os
try:
from unittest import mock
except ImportError:
import mock
def fake_os_unlink(path):
raise IOError("Testing!")
with mock.patch('os.unlink', fake_os_unlink):
os.unlink('foobar')
- 컨텍스트 관리자로 사용할 경우 mock.patch()는 대상 함수를 컨텍스트 내에서 실행되는 코드가 해당 패치 메서드를 사용할 수 있도록 제공하는 함수로 대체한다.
· 예시5 - mock.patch() 사용하여 일련의 동작 테스트하기
- mock.patch() 메서드를 사용하면 외부 코드 조각의 일부를 변경할 수 있으므로, 애플리케이션의 모든 조건을 테스트할 수 있는 방식으로 실행할 수 있다.
- 다음 예제는 웹 페이지(http://python.org)의 ‘Python is a programming language’라는 문자열의 모든 인스턴스를 검색하는 테스트 도구 모음을 구현한다.
from unittest.mock import patch, sentinel
try:
from unittest import mock
except ImportError:
import mock
import pytest
import requests
class WhereIsPythonError(Exception):
pass
def is_python_still_a_programming_language(): # 1
try:
r = requests.get("http://python.org")
except IOError:
pass
else:
if r.status_code == 200:
return 'Python is a programming language' in r.content
raise WhereIsPythonError("Something bad happened")
def get_fake_get(status_code, content):
m = mock.Mock()
m.status_code = status_code
m.content = content
def fake_get(url):
return m
return fake_get
def raise_get(url):
raise IOError("Unable to fetch url %s" % url)
@mock.patch('requests.get', get_fake_get(200, 'Python is a programming language for sure'))
def test_python_is():
assert is_python_still_a_programming_language() is True
@mock.patch('requests.get', get_fake_get(200, 'Python is no more a programming language'))
def test_python_is_not():
assert is_python_still_a_programming_language() is False
@mock.patch('requests.get', get_fake_get(404, 'whatever'))
def test_bad_status_code():
with pytest.raises(WhereIsPythonError):
is_python_still_a_programming_language()
@mock.patch('requests.get', raise_get)
def test_ioerror():
with pytest.raises(WhereIsPythonError):
is_python_still_a_programming_language()
- 페이지 자체를 수정하지 않고는 부정적인 시나리오를 테스트할 수 있는 방법은 없다. 이 경우 mock을 사용하여 요청의 동작을 변경하고 해당 문자열을 포함하지 않는 가짜 페이지로 가짜 응답을 반환하는 방식으로 문제를 해결할 수 있다.
- 이 예제는 mock.patch()의 데커레이터 버전을 사용한다. 데커레이터를 사용하면 mock 동작이 변경되지 않지만, 전체 테스트 함수의 컨텍스트 내에서 mock을 사용해야 하는 경우 더 간단하다.
- mock 객체를 사용하면 404 오류, I/O 오류, 네트워크 대기 시간 문제 등을 시뮬레이션할 수 있다.