개요
프로젝트의 유닛 테스트를 진행하면서 여러 비효율적인 상황을 볼 수 있다.
여기 어떤 기능을 실행하는데 오래 걸리는 전처리 작업
이 있다.
이 기능은 사용자의 요청이 발생하면 A -> B -> C
순서로 작업을 진행하는데, 문제는 A, B 작업이 너무 오래 걸리는 작업이라는 점이다.
테스트에서 A, B, C를 각각 테스트하고 싶지만 C를 실행할 때 테스트 시간이 너무 오래 걸리는 문제가 있다.
여기 또 다른 기능으로 외부 API 호출
이 있다.
이 기능은 위 상황처럼 오래 걸리지는 않지만, 하루에 호출할 수 있는 제한량이 있다. 문제는 해당 기능이 수십개의 다른 기능에 전처리 되는 호출이라, 직원들이 개발을 하다 테스트를 돌릴 때마다 해당 API가 호출되는 애로사항이 있다.
이런 상황은 비단 대형 프로젝트에서만 발생하는 것은 아니다.
가장 친숙한 예시는 외부 로그인 기능인 OAuth
일 것이다.
일반적으로 OAuth 기능은 프로젝트의 모든 기능에 우선적으로 실행될 것이고, 테스트 코드 수십개를 실행하면 한번에 수십개의 요청이 구글, 페이스북, 카카오로 날아갈 것이다.
그럼 당연히 요청 제한을 먹고 테스트는 실패하는 결과만 남아있다.
이런 상황을 해결해 줄 기능이 바로 mocking
이다.
Mocking 이란
Mock
은 개발한 제품을 테스트하는 과정에서 실제 모듈을 흉내내는 “가짜”를 작성하여 테스트의 효율성을 높이기 위한 개념이다.
이 가짜 모듈은 외부 API만 해당하는 것이 아니라, 현재 테스트하려는 모듈과 연결된 다른 모듈도 포함한다.
파이썬 unittest의 mock
파이썬 3.3부터 기본으로 제공하는 unittest.mock
이라는 모킹 라이브러리가 있다.
이 라이브러리를 통해서 기존 코드에 영향을 주지 않고 특정한 부분을 가짜로 대체할 수 있다.
1
from unittest import mock
테스트 메소드 mocking
예시 프로젝트 구조
1
2
3
4
5
6
from unittest import mock
...
@mock.patch("lib.csv_data.requests.get")
def test_function_mocking(self, mock_obj):
# 모킹 테스트
...
먼저 unittest.mock
라이브러리의 patch
함수를 가져왔다.
여기서 patch
함수를 데코레이터로 사용하여, mocking 경로
를 제공해주었다.
여기서 중요한 것은, 함수에 제공되는 경로는 mocking 하고자 하는 대상이 있는 경로 라는 점이다.
위 코드에서 모킹 하려는 대상은 requests.get
코드이고, 이는 lib/csv_data.py
파일에 있다.
모킹하려는 메소드의 실제 위치가 아닌, 그 메소드를 실제 호출하는 위치
라는 점을 기억하면 된다.
mocking 반환값 할당하기
1
2
3
4
5
6
from unittest import mock
...
@mock.patch("lib.csv_data.requests.get")
def test_function_mocking(self, mock_obj):
# 모킹 테스트
...
위 코드에서 테스트 메소드의 argement
로 mock_obj
가 추가된 것을 볼 수 있다.
이는 mock 객체를 의미하며 변수명은 상관없다. 여기서는 보기 편하게 mock_obj
로 설정하였다.
이 객체의 return_value
를 이용하여 어떤 결과값을 모킹할지 선언하여야 한다. 이 때 반환되는 객체의 method, property에 따라 코드가 다르다.
Mock 객체의
method
인 경우
1 2 3 4 mock_obj.return_value.json.return_value = { "PID": [113,241,323,124,105], "Cost": [10.5, 6.3, 2.6, 1.1, 9.7] }
Mock 객체의
property
인 경우
1 mock_obj.return_value.status_code = "200"
추가: mocking 반환값에 다른 객체 선언
다른 방법으로는 mock_obj.return_value
에 모킹한 기능의 원래 반환했어야 할 객체를 할당하는 방법도 있다.
1
2
3
4
5
6
7
8
...
fake_res = requests.models.Response()
fake_res.status_code = 200
fake_res.headers["Content-Type"] = "application/json"
fake_res.headers["Content-Length"] = "100"
...
mock_obj.return_value = fake_res
그 객체가 생성자를 가지고 있었다면 손쉽게 나타낼 수도 있지만, requests
는 해당되지 않는 것 같다.
기본적인 mock 객체 할당법이 메모리도 더 적게 소모하고 편하므로 그쪽을 쓰도록 하자.