Monkeypatching/mocking modules and environments

1 minute read

종종 테스트를 진행할 때 글로벌 세팅이나 네트워크 접근과 같은 상황에서 코드를 불러 와야하는 어려운 상황이 있습니다.
Pytest에서 제공하는 The monkeypatch fixture 는 이를 불러 오는데에 큰 역할을 합니다.

7.1 monkeypatching functions의 간단한 예제

os.expanduser가 특정 주소를 반환하게 하려면, the monkeypatch를 사용하면 됩니다. os.expanduser 호출하기 전에 setattr() 를 사용해야합니다. 예시:

# content of test_module.py
import os.path
def getssh(): # pseudo application code
    return os.path.join(os.path.expanduser("~admin"), '.ssh')

def test_mytest(monkeypatch):
    def mockreturn(path):
        return '/abc'
    monkeypatch.setattr(os.path, 'expanduser', mockreturn)
    x = getssh()
    assert x == '/abc/.ssh'

7.2 Global patch example: preventing “requests” from remote operations

requests라이브러리가 당신의 테스트 코드에서 http requests를 시행하는 것을 방지하기 위해서는 이렇게 하세요:

# content of conftest.py
import pytest
@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
    monkeypatch.delattr("requests.sessions.Session.request")

7.3 Monkeypatching 환경 변수

환경 변수를 가지고 작업을 한다면, 당신은 시스템에서 테스팅을 위해 안전하게 값을 변경하거나 지워야 할 것입니다. Monkeypatch는 이를 위해 setenv and 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 EnvironmentError."""
        username = os.getenv("USER")
       
        if username is None:
            raise EnvironmentError("USER environment is not set.")
        
        return username.lower()

두 가지 상황이 존재 할 수 있는데, 첫번째는 유저 환경 변수를 하나의 값으로 세팅 되어 있는 것입니다. 두번째는 유저 환경 변수가 존재하지 않는 상황입니다. 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 EnvironmentError is raised."""
monkeypatch.delenv("USER", raising=False)
with pytest.raises(EnvironmentError):
_ = get_os_user_lower()

This behavior can be moved into fixture structures and shared across tests:

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(EnvironmentError):
         _ = get_os_user_lower()