요즘 즐겁게 TDD 를 하며 Django 개발을 하는 중이다. 코드짜는게 너무 편하고 즐거워졌다!
그런데 mocking 을 하려는데 몇가지 문제가 생겨서 정리해보고자 한다.
결론부터 말하자면, 파이썬에서 namespace 로 import 해야하는 함수에 대한 mocking 은 불가능하다고 보는것이 맞아보인다..!
다음과 같은 구조로 프로젝트가 되어있다고 해보자. 프로젝트 루트에는 hello.py 가, hello/world/ 에는 amazing.py 가 있다.
다음은 hello/world/amazing.py 의 내용이다.
def printer():
print("hello world")
그리고 다음은 hello.py 의 내용이다.
from hello.world.amazing import printer
printer()
보통의 함수를 임포트 할 때의 모습이다. from - import -
같은 형식으로 임포트한다. 이런 방식을 namespace 임포트
라고 한다.
그런데 printer 함수를 mocking하고 싶다면, 보통은 이렇게 접근할것이다.
from unittest.mock import patch
from hello.world.amazing import printer
with patch('hello.world.amazing.printer', return_value=1) as mock_method:
def empty():
pass
mock_method.side_effect = empty
printer()
이렇게. 상식적이고 직관적이지 않은가? 함수의 위치를 넣고, mocking 시의 동작을 정의하고. side_effect 의 값을 함수 empty
로 주었기에, 저 코드의 실행결과는 'hello world' 가 되어서는 안된다.
그런데 저 코드를 저렇게 실행하면..
그렇다. 결과는 상식적이지 않다.. 그래서 이 문제로 삽질을 꽤 오래동안 했는데 답은 다음과 같이 얻을 수 있었다.
https://stackoverflow.com/questions/16134281/python-mocking-a-function-from-an-imported-module
When you are using the patch decorator from the unittest.mock package you are not patching the namespace the module is imported from
patch 데코레이터를 사용 할 때에는 module 이 임포트된 곳의 namespace 를 패치 하진 않는다
그렇다면 어떻게 모킹을 해야 할까? 답은 생각보다 간단하다. 지금 hello.py
의 입장에서 mocking 을 진행하는거다.
지금 저 printer
는 namespace 를 활용해 import 가 됐기 때문에, printer
는 hello.py
의 것인 상태인 것이다..! (그리고 우리가 실행하는 파일이기에 hello 는 __main__
이 된다)
즉 다음과 같이 접근하면 된다.
a(printer) 함수를 b(hello.py -> __main__) 코드에서 임포트하고 있다면, b(hello.py -> __main__) 의 a(printer) 를 mocking 하면 된다.
from unittest.mock import patch
from hello.world.amazing import printer
with patch('__main__.printer', return_value=1) as mock_method:
def empty():
pass
mock_method.side_effect = empty
printer()
사실 조금만 찾아보니 바로 답이 나왔는데 그 내용을 이해를 못했었다. 그래서 바보같은 해답을 적어뒀었는데 나같은 실수를 하는 사람이 또 있을까봐 바보같은 내용들을 밑에 숨겨둔다...
여기는 바보같이 접근했던 흔적...
- namespace 를 사용하지 않는다.
from unittest.mock import patch`
import hello.world.amazing
with patch('hello.world.amazing.printer', return\_value=1) as mock\_method:
def empty():
pass
mock_method.side_effect = empty
hello.world.amazing.printer()
- 이렇게 하면 아무런 출력값도 뜨지 않고, printer 에서 뱉는 값을 출력해봐도 정상적으로 1로 나온다.(return_value=1)
- 다만 이렇게 쓰는건 너무 불편하다. 테스트 코드를 위해서 본래 코드가 더러워지는건 올바른 접근 방법이 아닌거같다.
- class 안의 메소드를 mocking 한다
- 내가 선택한것은 이 방법이었다. 만약 mocking 하고자 하는 함수가 특정 클래스의 메소드를 호출하는 로직이 있고, 그 부분의 mocking 으로 대체 할 수 있다면 그렇게 진행하는것이 좋다.
- hello/world/amazing.py 가 다음과 같다고 하자.
class HelloWorldPrinter():
def execute(self):
print("hello world")
def printer():
hello\_world\_printer = HelloWorldPrinter()
hello\_world\_printer.execute()
- 그러면 다음과 같이, hello.py 에서 execute 를 mocking 하는것이다.
from unittest.mock import patch
from hello.world.amazing import printer
with patch('hello.world.amazing.HelloWorldPrinter.execute',
return_value=1) as mock_method:
def empty():
pass
mock_method.side_effect = empty
printer()
보통 mocking 이 필요한 경우는 특정 함수 전체의 동작을 바꿔야 할 때도 있지만 내부에서 있는 호출이 핵심인 경우도 꽤 많을거라 생각한다.
나의 경우에도 2로 충분히 대체가 가능했고. 만약 함수 하나가 핵심적인 로직이고 namespace 를 사용해서 import 가 된다면? 이 부분은 아직 잘 모르겠다. 더 공부가 필요할듯.
'Computer Science' 카테고리의 다른 글
Django 를 하는데 Signal 이 왔다! (Signal 관련 유의 사항) (0) | 2021.09.01 |
---|---|
Pytest 관련 짧은 팁 (0) | 2021.08.26 |
내 깃허브가 털렸다 (1) | 2021.04.09 |
Go 프로젝트 Docker 이미지 크기 99.2% 줄이기 (부제: 이미지 크기 12921% 떡상 시키기) (0) | 2021.04.08 |
이리치이고 저리치이고. 고난의 CI/CD 구축기 (0) | 2021.03.26 |