Dev/SW Engineering

[TDD] Python Testing Framework2 - Pytest, Unittest - mocking

HJChung 2021. 8. 16. 00:56

1. Mock

mocking이 필요한 경우를 예시로 들고, 해당 mocking을 Unittest로 구현할 때와 Pytest로 구현할 때를 나누어서 살펴보겠습니다. 

1) Random result mocking

import random
import requests

def roll_dice();
	print("rolling...")
    return random.randint(1, 6)
    
def quess_number(num):
	result = roll_dice()
    if result == num:
    	return "You won!"
    else:
    	return "You lost!"

이런 코드가 있을 때, Random 모듈이 잘 작동하는지가 아닌 random의 결과가 있을 때, 내가 작성한 guess_number 함수가 기대대로 잘 작동되는지 test해보고싶을 것입니다. 

①. Unittest로 구현 한 경우

Unittest의 mock라이브러리를 이용하여 Mocking을 통해 roll_dice을 mocking하고, 리턴값에 fake data를 지정해줌으로써 일정한 결과가 나올 수 있게 한 후 바로 원하는 부분만 테스트해 볼 수 있습니다. 

import unittest
from unittest import mock
from tests.unittest4.ex4 import roll_dice, guess_number, get_ip

class Mock_testing(unittest.TestCase):
  """
  이렇게 하면 roll_dice에서 계속 바뀌는 값을 주니까 guess_number를 test plan대로 test할 수가 없습니다. 
  그러기 때문에 ex4의 roll_dice를 mocking하고, 그 결과값을 정해줌으로써 test를 예상대로 진행할 수 있게 합니다. 
  """
  @mock.patch("tests.unittest4.ex4.roll_dice")
  def test_guess_number(self, mock_roll_dice): 
    mock_roll_dice.return_value = 3
    self.assertEqual(guess_number(3), 'You won!')
    self.assertEqual(guess_number(1), 'You lost!')
    mock_roll_dice.assert_called_with()

    args = mock_roll_dice.call_args
    args_list = mock_roll_dice.call_args_list
    print("Args: ", args)
    print("Args_list: ", args_list)

②. Pytest 와 unittest.mock을 사용하여 구현한 경우

Unittest.mock은 python2에서는 pip install mock으로 따로 설치가 필요하지만 python3.3부터는 표준 라이브러리로 별도 설치가 필요없습니다. 

from unittest import mock
import pytest
from tests.pytest4.ex4 import guess_number, get_ip

@pytest.mark.parametrize("_input,expected", [(3, "You won!"), (4, "You lost!")])
@mock.patch("tests.pytest4.ex4.roll_dice")
def test_guess_number(mock_roll_dice, _input, expected):
    mock_roll_dice.return_value = 3
    assert guess_number(_input) == expected
    mock_roll_dice.assert_called_once()

 

2) Request.get mocking

import requests

def get_ip():
	response = requests.get("https://httpbin.org/ip")
    if response.status_code == 200:
    	return response.json()['origin']

이런 코드를 테스트해보고 싶을 때 get 요청을 보내야지만 그 결과로 제가 작성한 코드가 잘 실행되는지 테스트 할 수 있습니다. 
이때도 외부 모듈인 request는 잘 작동한다는 가정하에, 저의 로직만 검사하면 되기 때문에 
사용되는 request.get을 patch로 mocking하고, 그 결과객체를 Mock으로 정해줌으로써 test를 예상대로 진행할 수 있게 합니다. 
이 때 return_value 역시 Mock으로 형태를 만들어줍니다. 

①.  Unittest로 구현 한 경우

import unittest
from unittest import mock
from tests.unittest4.ex4 import roll_dice, guess_number, get_ip

class Mock_testing(unittest.TestCase):
  @mock.patch("tests.unittest4.ex4.requests.get")    # HERE
  def test_get_ip(self, mock_request_get):
    mock_request_get.return_value = mock.Mock(name="mock response",
                                     **{"status_code": 200, "json.return_value": {"origin": "0.0.0.0"}}) # HERE
    self.assertEqual(get_ip(), "0.0.0.0")
    mock_request_get.assert_called_once_with("https://httpbin.org/ip")
    
    args = mock_request_get.call_args
    args_list = mock_request_get.call_args_list
    print("Args: ", args)
    print("Args_list: ", args_list)


if __name__ == '__main__':
  unittest.main()

 

실행 결과

 

3) Time.sleep result mocking

import time

def compute(x):
    response = expensive_api_call()
    return response + x

def expensive_api_call():
    time.sleep(1000) # takes 1,000 seconds
    return 123

이런 코드가 있을 때, compute 가 잘 작동하는지 test해보기 위해서 아래와 같이 코드를 짠다면 test시에도 1000ms를 기다려야 합니다. 

def test_compute():
    expected = 124
    actual = compute(1)
    assert expected == actual

 

This is too slow for a simple test.
생각해보면, 이 코드에서 테스트하고 싶은 부분은 time API가 잘 호출되는지가 아닌  API가 응답을 주었을 때 내가 작성한 코드가 기대대로 실행되는지에 대한 여부입니다.  이를 위해 Mocking을 통해 실질적으로 시간에 관련된, 즉, 몇 초간 기다려야 응답을 받을 수 있는 API에 대해서는 fake data를 return 하도록 함으로써 바로 원하는 부분만 테스트해 볼 수 있습니다. 

def api_call():
	time.sleep(3)
    return 9
    
def slow_function():
	api_result = api_call()
    # do some more stuff here
    return api_result

해당 코드의 test code를 작성해봅시다. 

. Unittest로 구현 한 경우 - patch.start(), patch.stop()

class Time_testing(unittest.Testcase):
	def setUp(self):
    	self.mocker = mock.patch("tests.unittest5.ex5.api_call")
        self.mockObject = (
        	self.mocker.start()
        )
        self.addCleanup(self.mocker.stop)
    
    def test_slow_function_mocked_api_call(self):
    	self.mockObject.return_value = 5
        self.assertEqual(slow_function(), 5)

여기서는 앞서 데코레이터를 사용하여 patch를 해주는게 아니라 patch로 만들어진 mock 객체에 있는 start, stop 메서드를 사용해본 것입니다. 공식문서에 따르면 start, stop을 활용하면 데코레이터를 중첩하거나 with문을 사용하지 않고도 여러 곳에서 patch를 간단하게 만들 수 있다는 장점이 있다고 합니다. 

그래서 일반적으로 start와 stop을 사용할 때는 아래의 코드처럼 TestCase의 setUp 메서드에서 여러 patch를 만들 때 입니다.

class MyTest(unittest.TestCase):
...     def setUp(self):
...         self.patcher1 = patch('package.module.Class1')
...         self.patcher2 = patch('package.module.Class2')
...         self.MockClass1 = self.patcher1.start()
...         self.MockClass2 = self.patcher2.start()
...
...     def tearDown(self):
...         self.patcher1.stop()
...         self.patcher2.stop()
...
...     def test_something(self):
...         assert package.module.Class1 is self.MockClass1
...         assert package.module.Class2 is self.MockClass2
...
>>> MyTest('test_something').run()

 

. Pytest와 Pytest-mock을 사용하여 구현한 경우

다음은 같은 test code를 pytest에서 pytest-mock 모듈을 사용하여서 mocking을 해 본 것입니다. 

Pytest-mock 모듈에 대해서는 자세히 알아보지는 못하였지만 pytest에서 mocking을 더욱 간편하게 해주는 역할을 한다고 생각합니다.

자세한 내용은 pytest-mock의 필요성에 대한 글인 <Packages needed for Mocking> 을 참고하였습니다.   

def test_slow_function_mocked_api_call(mocker):
    mocker.patch(
        'tests.pytest5.ex5.api_call',
        return_value=5
    )

    expected = 5
    actual = slow_function()
    assert expected == actual

 

4) Function mocking 

def square(value):
    return value ** 2

def cube(value): 
    return value ** 3

def main(value): 
    return square(value) + cube(value)

①.  Unittest로 구현 한 경우

import unittest
from unittest import mock

from tests.unittest7.ex7 import main


class TestNotMockedFunction(unittest.TestCase):
  @mock.patch('tests.unittest7.ex7.square')
  @mock.patch('tests.unittest7.ex7.cube')
  def test_main_function(self, mocked_square, mocked_cube):
      mocked_square.return_value = 1
      mocked_cube.return_value = 0
      self.assertEqual(main(5), 1)
      mocked_square.assert_called_once_with(5)
      mocked_cube.assert_called_once_with(5)
    

if __name__ == '__main__':
    unittest.main()

②. Pytest와 monkeypatch를 사용하여 구현한 경우

from tests.pytest7.ex7 import main

def test_main_function(monkeypatch):
    monkeypatch.setattr('tests.pytest7.ex7.square', lambda x: 1)
    monkeypatch.setattr('tests.pytest7.ex7.cube', lambda x: 0)
    assert main(5) == 1

monkeypatch.setattr원하는 테스트 동작으로 함수 또는 속성을 패치하는 데 사용 합니다. 

 

2. Mock, MagicMock, Patch

하면서 Mock와 MagicMock의 차이는 알겠는데 Mock와 Patch의 차이점이 잘 와닿지 않았습니다. 그래서 이를 비교한 글을 보고 조금이나마 이해 한 바를 정리하고자 합니다. 

  • Mock: 특정한 객체를 Mock하는 것으로 현재 scope 내의 객체를 대신합니다. 
  • MagicMock: 기본적으로 Mock() 만 사용하면 python의 매직 메서드가 자동으로 mocking되지는 않습니다. 그래서 매직 메서드가 필요하면 직접 할당을 해줘야하는 불편함이 있는데 MagicMock을 사용하면 매직 메서드를 미리 알아서 모킹해줍니다. 
>>> from unittest.mock import MagicMock

>>> mock = MagicMock()

>>> mock.__str__.return_value

"<MagicMock id='4556752144'>"

>>> mock.__str__.return_value = "I'm a magic mock."

>>> str(mock)

"I'm a magic mock."
  • Patch: 외부에서 import 해야하는 외부 라이브러리의 행위를 mocking합니다.

 

 


Reference

Random result mocking - Unittest / Pytest+unittest.mock

Request.get mocking - Unittest, call argument 검사

Time.sleep result mocking - Unittest / Pytest+Pytest-mock

Function mocking - Unittest / Pytest+monkeypatch

Mock, MagicMock, Patch