Patch.object로 간단하게 호출 테스트 하기
지난 unittest의 patch 글과 연관되어, 더욱 더 간단하게 호출 테스트를 할 수 있는 방법에 대해서 알아왔습니다.
patch.object(target, attribute)
pytest의 patch.object(target, attribute) 는 특정 객체의 속성을 테스트 중에 임시로 Mock 등으로 교체하는 기능입니다.
target은 클래스나 인스턴스와 같은 객체이고, attribute는 문자열로 된 속성 이름입니다.
new 인자 유무
특이점으로는 new 인자를 지정하지 않으면 MagicMock이 자동으로 생성되어 해당 속성이 임시로 대체되고, new에 직접 Mock, 값, 함수 등을 전달해 교체가 가능합니다.
무슨 말이냐면
from unittest.mock import patch
class MyClass:
def foo(self):
return 10
with patch.object(MyClass, 'foo'):
result = MyClass().foo() # None 반환
여기서 foo는 MagicMock으로 임시 대체되고, 자동으로 MagicMock() 객체를 만들어서 속성에 할당합니다. 즉 원래 함수나 값 대신 MagicMock()이 적용되어 호출하면 아무런 에러 없이 호출이 가능하고, 호출 횟수나 인자 등도 추적이 가능합니다.
예를 들어서 해당 테스트에서 service에 주입된 핸들러인 notifier의 notify_error 메서드가 호출이 된 지 알기 위해서 다음과 같은 코드를 짤 수 있습니다.
with patch.object(service._notifier, 'notify_error') as mock_notify:
service.process()
mock_notify.assert_not_called()
만약 notify_error가 호출되었다면 AssertionError가 발생합니다.
반대로 호출이 되어야 하는 로직의 경우 다음과 같이 코드를 작성합니다.
with patch.object(service._notifier, 'notify_error') as mock_notify:
service.process()
mock_notify.assert_called()
정확히 한번 호출되었는지 알기 위해서는 assert_called_once 를 사용할 수도 있고,
만약 new 인자를 넣게 되면 특정 값을 반환할 수 있습니다.
# 1) 함수 직접 대체
with patch.object(MyClass, 'foo', new=lambda self: 99):
print(MyClass().foo()) # 99
# 2) 특정 숫자(값)로 대체
with patch.object(MyClass, 'foo', new=123):
print(MyClass().foo) # 123
# 3) Mock 객체로 대체도 가능하다!
from unittest.mock import Mock
my_mock = Mock(return_value=77)
with patch.object(MyClass, 'foo', new=my_mock):
print(MyClass().foo()) # 77
pytest에서는?
def test_my_method(mocker):
my_object = MyClass()
mock_method = mocker.Mock(return_value=24)
mocker.patch.object(my_object, 'my_method', mock_method)
assert my_object.my_method() == 24
mock_method.assert_called_once()
보통 이런식으로 mocker.patch.object를 사용한다고 합니다. 근데 저는 컨텍스트 매니저로 사용하는게 시작과 끝을 쉽게 알 수 있어 가독성이 좋은 것 같네요!