회원가입

4. 테스트 모킹 patch

NULL 2022-05-15

 

실전에서 더 많이 쓰이는 unittest.mock 모듈의 patch() 데코레이터를 이용한 Unit Test 를 알아보자.

 

patching? mocking?


unittest.mock 모듈의 patch() 데코레이터를 이용하면 특정 모듈의 함수나 클래스를 가짜(mock) 객체 MagicMock 인스턴스로 대체할 수 있다.

이 과정을 흔히 mocking 또는 patching 이라고 한다.

 

Unit Test 를 작성할 때 외부 서비스에 의존하지 않고 독립적으로 실행이 가능한 Unit Test 를 작성하기 위해 사용되는 테스팅 기법이다.

 

 

patch 데코레이터


unittest.mock 모듈의 patch() 데코레이터는 특정 범위 내에서만 mocking 이 가능하도록 해준다.

일반적으로 아래 코드와 같이 patching 이 필요한 Unit Test 메서드에 patch() 데코레이터를 선언해줌으로써 해당 매서드 내에서만 patching 이 이뤄지게 한다.

 

test_us.py

from unittest import TestCase, main
from unittest.mock import patch

def hello():
    return "Hello!"

class TestMe(TestCase):
    @patch("__main__.hello", return_value="Mock!")
    def test_hello(self, mock_hello):
        self.assertEqual(hello(), "Mock!")
        self.assertIs(hello, mock_hello)
        mock_hello.assert_called_once_with()

if __name__ == "__main__":
    main()

 

 

위 예제를 보면 원래 "Hello!" 을 리턴하는 hello() 함수가 "Mock!" 를 대신 리턴하도록 @patch() 데코레이터로 patching 을 하고 있다.

@patch("__main__.hello", return_value="Mock!")

 

@patch() 데코레이터는 첫번째 인자로 patching 할 메서드를 package.module.Class.method 형태의 문자열로 받는다.

본 예제에서는 patching 할 메서드가 같은 모듈에 있기 때문에 __main__ 모듈명을 사용하고 있다.

(@patch("test_us.hello", return_value="Mock!") 와 같은 의미다)

@patch("__main__.hello", ...)

 

@patch() 데코레이터를 사용해서 patching 을 하면 Mock 객체를 테스트 메서드의 인자로 추가되는데 바로 mock_hello 이 이 Mock 객체의 매개변수 명으로 쓰이고 있다.

def test_hello(self, mock_hello):

 

테스트 메서드에서 검증하는 내용을 보면 hello() 함수를 호출했을 때 원래 리턴 값인 "Hello!" 대신에 "Mock!" 을 리턴하는지 검사한다.

self.assertEqual(hello(), "Mock!")

 

그리고 정말로 hello() 함수가 mock_hello() 함수로 대체가 되었는지 확인한다.

self.assertIs(hello, mock_hello)

 

그리고 Mock 객체에 함수 호출이 기억되었는지를 검증하고 있다.

mock_hello.assert_called_once_with()

 

 

실전 예제


@patch() 데코레이터는 외부 서비스에 의존하는 코드에 대한 테스트를 작성할 때 유용하게 쓰인다.

예를 들어, API 를 호출하는 코드에 대한 테스트를 작성할 때, 실제로 네트워크 연동을 하면 테스트가 느려지고 깨지기 쉽다.

 

지금부터 아래와 같이 requests 패키지를 사용하여 외부 API 와 연동하여 사용자를 조회하거나 생성해주는 간단한 모듈에 대한 Unit Test 를 작성해보겠다.

 

user_manage.py

import requests

def get_user(id):
    res = requests.get(f"<https://jsonplaceholder.typicode.com/users/{id}>")
    if res.status_code != 200:
        raise Exception("Failed to get a user.")
    return res.json()

def create_user(user):
    res = requests.post(f"<https://jsonplaceholder.typicode.com/users>", data=user)
    if res.status_code != 201:
        raise Exception("Failed to create a user.")
    return res.json()

 

get_user() 함수에 대한 테스트를 작성해보겠다.

get_user() 함수는 인자로 넘어온 사용자 id를 이용해서 URL을 만든 후, 이 URL을 인자로 넘겨 requests 패키지의 get() 함수를 호출한다.

그리고 requests 패키지의 get() 함수의 리턴 객체의 json() 함수를 호출한 결과를 리턴한다.

 

실제 네트워크 연동이 발생하지 않는 Unit Test 를 작성하려면 requests 패키지의 get() 함수를 patching 해줘야 한다.

즉, requests.get() 함수를 Mock 객체로 교체하고, 그 Mock 객체가 어떻게 작동할지 설정한 다음, 실제로 Mock 객체 대상으로 예상했던 작업이 일어났는지 검증해야 한다.

 

1. get_user 테스트

from unittest import TestCase
from unittest.mock import patch

import user_manager

class TestUserManager(TestCase):
    @patch("requests.get")
    def test_get_user(self, mock_get):
        res = mock_get.return_value
        res.status_code = 200
        res.json.return_value = {
            "name": "Test User",
            "email": "user@test.com",
        }

        user = user_manager.get_user(1)
        
        self.assertEqual(user["name"], "Test User")
        self.assertEqual(user["email"], "user@test.com")
        mock_get.assert_called_once_with("<https://jsonplaceholder.typicode.com/users/1>")

 

테스트 함수에 @patch("requests.get") 데코레이터를 선언하면, mock_get 매개 변수에 교체된 Mock 객체가 할당된다.

@patch("requests.get")
def test_get_user(self, mock_get):

 

이제 mock_get 에 할당되어 있는 Mock 객체가 리턴할 객체의 status_code 속성을 200 으로 지정하고, json 메서드의 리턴 객체를 임의의 사용자로 지정한다.

res = mock_get.return_value
res.status_code = 200
res.json.return_value = {
    "name": "Test User",
    "email": "user@test.com",
}

 

다음으로 user_manager 모듈의 get_user() 함수를 호출하여, 내부적으로 requests 모듈의 get() 함수가 호출되게 한다.

user = user_manager.get_user(1)

 

그 다음, get_user() 함수를 호출 결과가 임의의 사용자에 담긴 내용과 일치하는지 확인한다.

self.assertEqual(user["name"], "Test User")
self.assertEqual(user["email"], "user@test.com")

 

마지막으로 Mock 객체가 get_user() 함수에 인자로 넘어간 사용자 id 를 포함하는 정확한 URL 로 호출이 되었는지 검증한다.

mock_get.assert_called_once_with("<https://jsonplaceholder.typicode.com/users/1>")

 

???? patchingrequests.get 와 같은 것을 내가 만든 가짜 함수로 만들겠다! 라는 의미인 것 같다.

 

2. create_user 테스트

from unittest import TestCase
from unittest.mock import patch

import user_manager

class TestUserManager(TestCase):
    @patch("requests.post")
    def test_create_user(self, mock_post):
        res = mock_post.return_value
        res.status_code = 201
        res.json.return_value = {"id": 99}

        user = user_manager.create_user(
            {"name": "Test User", "email": "user@test.com",}
        )
   
        self.assertEqual(user["id"], 99)
        mock_post.assert_called_once_with(
            "<https://jsonplaceholder.typicode.com/users>",
            data={"name": "Test User", "email": "user@test.com",},
        )

create_user() 함수도 위와 같이 비슷한 방식으로 Unit Test 를 작성할 수 있다.

requests 패키지의 post() 함수를 patching 하고 Mock 객체가 할당된 mock_post 를 같은 방식으로 활용하면 된다.

 

참고 링크: https://www.daleseo.com/python-unittest-mock-patch/

0 0
테스트
테스트를 작성해야 하는 이유는 정말 너무 많다. 개발 관점으로 다양한 테스팅에 과정에 대해 필자가 공부한 것을 토대로 포스팅한 게시판이다.
Yesterday: 1
Today: 249