代码之家  ›  专栏  ›  技术社区  ›  sjaymj62

Python MonkeyPatching最佳实践

  •  1
  • sjaymj62  · 技术社区  · 6 年前

    我正在测试一个具有多个外部依赖项的应用程序,我使用MonkeyPatching技术通过自定义实现来帮助我的测试修补外部库的功能。它按预期工作。

    但我现在遇到的问题是,这使得我的测试文件非常混乱。我有几个测试,每个测试都需要自己实现补丁函数。

    例如,假设我有一个来自外部库的get函数, test_a() 需要 GET() 以使其返回错误和 test_b() 需要 获取() 以使它返回真。

    处理这种情况的首选方法是什么?目前我做以下工作:

    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部件移动到单独的文件中吗?

    1 回复  |  直到 6 年前
        1
  •  1
  •   hoefling    6 年前

    不知道更多细节,我建议分开 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 在您的情况下),测试或设备参数化通常可以节省大量的代码复制。假设您有一个调用 得到 内部:

    # my_lib.py
    
    def inform():
        try:
            result = ExternalLib.GET()
        except Exception:
            return 'error'
        if result:
            return 'success'
        else:
            return 'failure'
    

    你想测试它是否返回一个有效的结果,不管是什么 得到 行为:

    # test_my_lib.py
    
    def test_inform():
        assert inform() in ['success', 'failure', 'error']
    

    使用上述方法,您需要复制 test_inform 三次,复印件之间唯一的区别是使用了不同的夹具。这可以通过编写一个可接受多个补丁的参数化夹具来避免。 得到 :

    @pytest.fixture(params=[lambda: True,
                            lambda: False,
                            raise_],
                    ids=['pos', 'neg', 'exception'])
    def mocked_GET(request):
        monkeypatch.setattr(ExternalLib, 'GET', request.param)
    

    现在申请时 mocked_GET 测试通知 :

    @pytest.mark.usefixtures('mocked_GET')
    def test_inform():
        assert inform() in ['success', 'failure', 'error']
    

    一个测试中有三个测试: 测试通知 将运行三次,每次模拟传递到 嘲弄 参数。

    test_inform[pos]
    test_inform[neg]
    test_inform[exception]
    

    测试也可以参数化(通过 pytest.mark.parametrize )正确应用时,参数化技术节省了大量样板代码。