Python monkeypatching 最佳实践
Python monkeypatching best practices
我正在测试一个具有多个外部依赖项的应用程序,并且我使用了 monkeypatching 技术通过自定义实现来修补外部库的功能,以帮助我的测试。它按预期工作。
但我目前遇到的问题是,这让我的测试文件变得非常混乱。我有几个测试,每个测试都需要自己实现修补函数。
例如,假设我有一个来自外部库的 GET 函数,我的 test_a()
需要修补 GET()
以便 returns False 和 test_b()
需要 GET()
进行修补,以便 returns 正确。
处理这种情况的首选方法是什么。目前我在做以下事情:
def test_a(monkeypatch):
my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)
def test_b(monkeypatch)
my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)
def test_c(monkeypatch)
my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = True)
def my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = False):
def patch_func_pos():
return True
patch_func_neg():
return False
patch_func_exception():
raise my_exception
if patch_get_to_return_true:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_pos)
if patch_get_to_return_false:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_neg)
if patch_get_to_raise_exception:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_exception)
上面的示例只有三个测试来修补一个函数。我的实际测试文件有大约20个测试,每个测试都会进一步修补几个功能。
有人可以建议我更好的处理方法吗?是否建议将 monkeypatching 部分移动到单独的文件中?
在不知道更多细节的情况下,我建议将 my_patcher
分成几个小装置:
@pytest.fixture
def mocked_GET_pos(monkeypatch):
monkeypatch.setattr(ExternalLib, 'GET', lambda: True)
@pytest.fixture
def mocked_GET_neg(monkeypatch):
monkeypatch.setattr(ExternalLib, 'GET', lambda: False)
@pytest.fixture
def mocked_GET_raises(monkeypatch):
def raise_():
raise Exception()
monkeypatch.setattr(ExternalLib, 'GET', raise_)
现在使用 pytest.mark.usefixtures
在测试中自动应用夹具:
@pytest.mark.usefixtures('mocked_GET_pos')
def test_GET_pos():
assert ExternalLib.GET()
@pytest.mark.usefixtures('mocked_GET_neg')
def test_GET_neg():
assert not ExternalLib.GET()
@pytest.mark.usefixtures('mocked_GET_raises')
def test_GET_raises():
with pytest.raises(Exception):
ExternalLib.GET()
不过,还有改进的余地,要看实际情况。例如,当测试逻辑相同并且唯一不同的是某些测试先决条件(例如在您的情况下 GET
的不同补丁)时,测试或固定装置参数化通常会节省大量代码重复。假设您有一个内部调用 GET
的函数:
# my_lib.py
def inform():
try:
result = ExternalLib.GET()
except Exception:
return 'error'
if result:
return 'success'
else:
return 'failure'
并且您想测试它 returns 是否是一个有效的结果,无论 GET
的行为如何:
# test_my_lib.py
def test_inform():
assert inform() in ['success', 'failure', 'error']
使用上述方法,您需要复制 test_inform
三次,副本之间的唯一区别是使用了不同的夹具。这可以通过编写一个参数化的夹具来避免,该夹具将接受 GET
:
的多个补丁可能性
@pytest.fixture(params=[lambda: True,
lambda: False,
raise_],
ids=['pos', 'neg', 'exception'])
def mocked_GET(request):
monkeypatch.setattr(ExternalLib, 'GET', request.param)
现在将 mocked_GET
应用于 test_inform
时:
@pytest.mark.usefixtures('mocked_GET')
def test_inform():
assert inform() in ['success', 'failure', 'error']
您可以从一个测试中获得三个测试:test_inform
将 运行 三次,每个模拟一次传递给 mocked_GET
参数。
test_inform[pos]
test_inform[neg]
test_inform[exception]
测试也可以参数化(通过 pytest.mark.parametrize
),如果正确应用,参数化技术可以节省大量样板代码。
我正在测试一个具有多个外部依赖项的应用程序,并且我使用了 monkeypatching 技术通过自定义实现来修补外部库的功能,以帮助我的测试。它按预期工作。
但我目前遇到的问题是,这让我的测试文件变得非常混乱。我有几个测试,每个测试都需要自己实现修补函数。
例如,假设我有一个来自外部库的 GET 函数,我的 test_a()
需要修补 GET()
以便 returns False 和 test_b()
需要 GET()
进行修补,以便 returns 正确。
处理这种情况的首选方法是什么。目前我在做以下事情:
def test_a(monkeypatch):
my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)
def test_b(monkeypatch)
my_patcher(monkeypatch, patch_get_to_return_true = True, patch_get_to_return_false = False, patch_get_to_raise_exception = False)
def test_c(monkeypatch)
my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = True)
def my_patcher(monkeypatch, patch_get_to_return_true = False, patch_get_to_return_false = False, patch_get_to_raise_exception = False):
def patch_func_pos():
return True
patch_func_neg():
return False
patch_func_exception():
raise my_exception
if patch_get_to_return_true:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_pos)
if patch_get_to_return_false:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_neg)
if patch_get_to_raise_exception:
monkeypatch.setattr(ExternalLib, 'GET', patch_func_exception)
上面的示例只有三个测试来修补一个函数。我的实际测试文件有大约20个测试,每个测试都会进一步修补几个功能。
有人可以建议我更好的处理方法吗?是否建议将 monkeypatching 部分移动到单独的文件中?
在不知道更多细节的情况下,我建议将 my_patcher
分成几个小装置:
@pytest.fixture
def mocked_GET_pos(monkeypatch):
monkeypatch.setattr(ExternalLib, 'GET', lambda: True)
@pytest.fixture
def mocked_GET_neg(monkeypatch):
monkeypatch.setattr(ExternalLib, 'GET', lambda: False)
@pytest.fixture
def mocked_GET_raises(monkeypatch):
def raise_():
raise Exception()
monkeypatch.setattr(ExternalLib, 'GET', raise_)
现在使用 pytest.mark.usefixtures
在测试中自动应用夹具:
@pytest.mark.usefixtures('mocked_GET_pos')
def test_GET_pos():
assert ExternalLib.GET()
@pytest.mark.usefixtures('mocked_GET_neg')
def test_GET_neg():
assert not ExternalLib.GET()
@pytest.mark.usefixtures('mocked_GET_raises')
def test_GET_raises():
with pytest.raises(Exception):
ExternalLib.GET()
不过,还有改进的余地,要看实际情况。例如,当测试逻辑相同并且唯一不同的是某些测试先决条件(例如在您的情况下 GET
的不同补丁)时,测试或固定装置参数化通常会节省大量代码重复。假设您有一个内部调用 GET
的函数:
# my_lib.py
def inform():
try:
result = ExternalLib.GET()
except Exception:
return 'error'
if result:
return 'success'
else:
return 'failure'
并且您想测试它 returns 是否是一个有效的结果,无论 GET
的行为如何:
# test_my_lib.py
def test_inform():
assert inform() in ['success', 'failure', 'error']
使用上述方法,您需要复制 test_inform
三次,副本之间的唯一区别是使用了不同的夹具。这可以通过编写一个参数化的夹具来避免,该夹具将接受 GET
:
@pytest.fixture(params=[lambda: True,
lambda: False,
raise_],
ids=['pos', 'neg', 'exception'])
def mocked_GET(request):
monkeypatch.setattr(ExternalLib, 'GET', request.param)
现在将 mocked_GET
应用于 test_inform
时:
@pytest.mark.usefixtures('mocked_GET')
def test_inform():
assert inform() in ['success', 'failure', 'error']
您可以从一个测试中获得三个测试:test_inform
将 运行 三次,每个模拟一次传递给 mocked_GET
参数。
test_inform[pos]
test_inform[neg]
test_inform[exception]
测试也可以参数化(通过 pytest.mark.parametrize
),如果正确应用,参数化技术可以节省大量样板代码。