Mock 사용해보기(Unittest)

hodu·2022년 12월 9일
0

python

목록 보기
16/17
post-thumbnail

Mock이 뭔데?

mock은 "모조품"이라는 뜻이다.
구글에 "what is pytest mock?"라고 치면 이렇게 나온다

pytest에서 mocking은 함수 내에서 함수의 반환 값을 대체할 수 있습니다.
이는 원하는 함수를 테스트하고 테스트 중인 원하는 함수 내에서 중첩 함수의 반환 값을 대체하는 데 유용합니다.(번역)

몬소리야...

아무튼 무언가 모조품을 만들고 이 모조품을 통해 실제 객체를 호출하지 않더라도 테스트가 가능하도록 만들어주는 것이다.

이해를 위해 예를 들자면 user를 만드는 create_user라는 함수가 제대로 동작하는지 확인하기 위해서는 우리는 실제 DB에 어떤 값을 보내야 할 것이다.

# 망한 코드
def test_create_user():
    response = client.post(
        "/api/user/",
        json={
            "user_id": "test_user",
            "nickname": "test_user",
            "password": "testuser1",
            "password_check": "testuser1",
        },
    )
    assert response.status_code == 201
    assert response.json() == {"detail": "사용자가 생성되었습니다."}

이런 식으로 테스트 코드를 작성해야 할 텐데, 문제가 있다.

🚨 한 번 테스트하면 다시 사용할 수 없음!

user_id 값과 nickname은 unique 한 값이다. 그래서 이 테스트코드를 한 번 돌리고 나면 중복 에러로 인해 테스트 에러가 뜨게된다.
나는 해결 방법으로 random이나 uuid를 사용해볼까 생각했었다.

그러면 이 문제를 어떻게 해결하냐?

" MOCK을 사용한다! "

mock을 사용해야 하는 또 다른 이유

Mock을 사용해야 하는 또 다른 이유는 다음과 같다.

1. 요청이 오래 걸려서..

unittest, pytest는 매우 빠르다.
그래서 sleep이나 http request를 통해 "기다려야"하는 상황에서 너무 오래 걸리게 된다면 지체없이 끝내버리고 OK를 던져버린다.

결론은 이 테스트가 진짜로 성공했는지 실패했는지 모른채로 끝나게 된다.

2. 종속성

def a():
	return 'hello world'
    
def b():
	return a

나는 b라는 함수를 테스트해보고 싶은데, a라는 함수가 무조건적으로 호출되어야 하는 상황이다. 이런 상황을 "종속성"이라고 하며, mock을 사용하면 해결이 된다.

이런 이유들 때문에 우리는 Mock을 사용하게 된다.

Mock 사용하기

그럼 이론은 어느정도 알게되었으니 본격적으로 사용해보자.

main.py에 아래와 같은 코드가 있을 때, 이 코드를 mock을 이용해서 테스트 해보자.

# main.py
import requests

class Blog:
    def __init__(self, name):
        self.name = name

    def posts(self):
        response = requests.get("https://jsonplaceholder.typicode.com/posts")

        return response.json()

    def __repr__(self):
        return '<Blog: {}>'.format(self.name)

마찬가지로 아래와 같은 테스트 코드를 작성하자.

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


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title',
                'body': 'Far out in the uncharted backwaters of the unfashionable  end  of the  western  spiral  arm  of  the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)
        self.assertEqual(response[0]['userId'], 1)
        self.assertEqual(response[0]['title'], 'Test Title')

데코레이터 @patch를 사용하게 되면 이 안에 들어있는 함수, 클래스의 mock(모조품)이 반환되어 데코레이터 된 함수의 인수로 전달된다.

즉, 여기서는 MockBlog라는 이름을 통해 main.Blog의 모조품이 전달받았고 이걸 blog라는 변수에다 저장을 한 것이다.

그리고 return_value를 이용하여 어떤 값이 돌아올 것인지도 지정하게 된다.


?

이걸 쓰다보면 이걸 왜 하지? 라는 의문점이 들 것이다.
(특히 이부분)

self.assertEqual(response[0]['userId'], 1)
self.assertEqual(response[0]['title'], 'Test Title')

내가 return_value를 지정해주는데, reponse를 왜 검사하지?
실제로 posts()라는 객체로 들어갔다 나오는 것도 아닌데 이걸 왜 검사할까?

다시 돌아와서

Blog 클래스에 있는 posts라는 함수는 requests를 이용한 테스트이다. 그래서 mock을 이용해주어야 한다.

(추가)우리는 위에서 "종속성"을 해결해주기 위해서 mock을 사용한다고 했다.

def a():
	return 'hello world'
    
def b():
	return a

위와 같은 코드를 테스트하기 위해서는 아래와 같이 작성하면 된다.

from main import b 

def test_mocking_function(mocker): 
    mocker.patch( "main.a" , return_value='hello world') 
    
    assert dummy_function() == 'hello world'

즉 a라는 함수가 무엇을 받아올 것인지 테스트코드에서 정의해주고, b 함수를 테스트 할 때 mock을 이용해 종속성을 해소(?)한다.

잉? 그럼 아까 그 코드는?

self.assertEqual(response[0]['userId'], 1)
self.assertEqual(response[0]['title'], 'Test Title')

이건 필요없는 코드다. 굳이 할 필요가 없는 코드임.

후기

후.. 어렵다. pytest처럼 하면 되겠지 하고 가볍게 생각했는데, 쓰다보니 왜 쓰는지도 잘 모르겠고, 어떻게 쓰는지도 정확하게 모르겠어서 이 간단한 문제를 지금 벌써 3일째 헤매는 중이다..ㅠㅠ
아직도 해결중이라 관련해서 알게되는 내용마다 추가할 예정!

참고 링크

profile
안녕 세계!

0개의 댓글