1. Monkeypatching 이란?
공식 문서에는 다음과 같이 Monkeypatching을 설명한다. 항상 특정 값을 반환하도록 만들 때 monkeypatch를 사용하라고.
아래의 예에서 monkeypatch.setattr은 Path.home을 패치하는 데 사용되므로 테스트가 실행될 때 ('/abc')가 항상 사용된다. 이것은 테스트 목적으로 실행 중인 사용자에 대한 의존성을 제거한다.(사용자의 환경에 따라 테스트 결과가 달라질 수 있는데 이를 삭제하기 위한 방법) monkeypatch.setattr은 패치된 함수를 사용할 함수를 호출하기 전에 호출해야 한다. 테스트 기능이 완료되면 Path.home 수정이 실행 취소된다.
# contents of test_module.py with source code and the test
from pathlib import Path
def getssh():
"""Simple function to return expanded homedir ssh path."""
return Path.home() / ".ssh"
def test_getssh(monkeypatch):
# mocked return function to replace Path.home
# always return '/abc'
def mockreturn():
return Path("/abc")
# Application of the monkeypatch to replace Path.home
# with the behavior of mockreturn defined above.
monkeypatch.setattr(Path, "home", mockreturn)
# Calling getssh() will use mockreturn in place of Path.home
# for this test with the monkeypatch.
x = getssh()
assert x == Path("/abc/.ssh")
2. Monkeypatching returned objects: building mock classes
monkeypatch.setattr은 클래스와 함께 사용하여 값 대신 함수에서 반환된 객체를 모의실험 할 수 있다. API URL을 가져와서 json 응답을 반환하는 간단한 함수를 상상해 보자.
# contents of test_app.py, a simple test for our API retrieval
# import requests for the purposes of monkeypatching
import requests
# our app.py that includes the get_json() function
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
return {"mock_key": "mock_response"}
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse()
# apply the monkeypatch for requests.get to mock_get
monkeypatch.setattr(requests, "get", mock_get)
# app.get_json, which contains requests.get, uses the monkeypatch
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
monkeypatch는 mock_get 함수를 사용하여 mock for requests.get을 적용한다. mock_get 함수는 MockResponse 클래스의 인스턴스를 반환합니다. MockResponse 클래스는 알려진 테스트 사전을 반환하도록 정의된 json() 메서드를 가지며 외부 API 연결이 필요하지 않는다. 테스트 중인 시나리오에 적합한 복잡도로 MockResponse 클래스를 만들 수 있다. 예를 들어, 항상 True를 반환하는 ok 속성을 포함하거나 입력 문자열을 기반으로 하는 json() 메서드에서 다른 값을 반환할 수 있다.
이 mock은 fixutre를 사용하여 테스트 간에 공유도 할 수 있다.
# contents of test_app.py, a simple test for our API retrieval
import pytest
import requests
# app.py that includes the get_json() function
import app
# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
def json():
return {"mock_key": "mock_response"}
# monkeypatched requests.get moved to a fixture
@pytest.fixture
def mock_response(monkeypatch):
"""Requests.get() mocked to return {'mock_key':'mock_response'}."""
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr(requests, "get", mock_get)
# notice our test uses the custom fixture instead of monkeypatch directly
def test_get_json(mock_response):
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
3. Monkeypatching environment variables
환경 변수로 작업하는 경우 테스트 목적으로 시스템에서 값을 안전하게 변경하거나 삭제해야 하는 경우가 많다. monkeypatch는 setenv 및 delenv 방법을 사용하여 이를 수행할 수 있는 메커니즘을 제공한다.
# contents of our original code file e.g. code.py
import os
def get_os_user_lower():
"""Simple retrieval function.
Returns lowercase USER or raises OSError."""
username = os.getenv("USER")
if username is None:
raise OSError("USER environment is not set.")
return username.lower()
두 가지 경로가 있다. 먼저 USER 환경 변수가 값으로 설정된다. 둘째, 사용자 환경 변수가 존재하지 않는 것. monkeypatch를 사용하면 실행 환경에 영향을 주지 않고 두 경로를 안전하게 테스트할 수 있다:
# contents of our test file e.g. test_code.py
import pytest
def test_upper_to_lower(monkeypatch):
"""Set the USER env var to assert the behavior."""
monkeypatch.setenv("USER", "TestingUser")
assert get_os_user_lower() == "testinguser"
def test_raise_exception(monkeypatch):
"""Remove the USER env var and assert OSError is raised."""
monkeypatch.delenv("USER", raising=False)
with pytest.raises(OSError):
_ = get_os_user_lower()
이것 또한 fixture로 공유할 수 있다.
# contents of our test file e.g. test_code.py
import pytest
@pytest.fixture
def mock_env_user(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
@pytest.fixture
def mock_env_missing(monkeypatch):
monkeypatch.delenv("USER", raising=False)
# notice the tests reference the fixtures for mocks
def test_upper_to_lower(mock_env_user):
assert get_os_user_lower() == "testinguser"
def test_raise_exception(mock_env_missing):
with pytest.raises(OSError):
_ = get_os_user_lower()
4. Monkeypatching dictionaries
monkeypatch.setitem을 사용하여 테스트 중에 사전의 값을 특정 값으로 안전하게 설정할 수 있다. 예를 들면:
# contents of app.py to generate a simple connection string
DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
def create_connection_string(config=None):
"""Creates a connection string from input or defaults."""
config = config or DEFAULT_CONFIG
return f"User Id={config['user']}; Location={config['database']};"
테스트를 위해 DEFAULT_CONFIG 사전을 특정 값에 패치할 수 있다.
# contents of test_app.py
# app.py with the connection string function (prior code block)
import app
def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
# expected result based on the mocks
expected = "User Id=test_user; Location=test_db;"
# the test uses the monkeypatched dictionary settings
result = app.create_connection_string()
assert result == expected
삭제나 fixture는 다음과 같다.
delitem:
# contents of test_app.py
import pytest
# app.py with the connection string function
import app
def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# Key error expected because a config is not passed, and the
# default is now missing the 'user' entry.
with pytest.raises(KeyError):
_ = app.create_connection_string()
fixture:
# contents of test_app.py
import pytest
# app.py with the connection string function
import app
# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
"""Set the DEFAULT_CONFIG user to test_user."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@pytest.fixture
def mock_test_database(monkeypatch):
"""Set the DEFAULT_CONFIG database to test_db."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
@pytest.fixture
def mock_missing_default_user(monkeypatch):
"""Remove the user key from DEFAULT_CONFIG"""
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string()
assert result == expected
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()
참고
https://docs.pytest.org/en/7.2.x/how-to/monkeypatch.html#monkeypatching
'Python' 카테고리의 다른 글
결합과 추상화 - 3 (0) | 2023.02.21 |
---|---|
결합과 추상화 - 2 (0) | 2023.02.13 |
Pytest - 2. Fixture 알아보기 (0) | 2023.02.01 |
Pytest 사용하기 - 1. 간단한 예제 (0) | 2023.01.31 |
파이썬으로 배우는 자료구조 핵심 원리 (0) | 2023.01.30 |