如何为Python单元测试提供模拟类方法?

51

假设我有一个像这样的类。

class SomeProductionProcess(CustomCachedSingleTon):
    
    @classmethod
    def loaddata(cls):
        """
        Uses an iterator over a large file in Production for the Data pipeline.
        """
        pass

现在在测试时,我想要修改 loaddata() 方法内部的逻辑。这将是一个简单的自定义逻辑,不处理大数据。

如何使用 Python Mock UnitTest 框架在测试时提供 loaddata() 的自定义实现?


1
请注意,loaddata()是一个@classmethod。另外,我想提供一个自定义的loaddata()来测试这个类中的另一个方法。 - Vineel
1
“Mock方法”更符合问题的要求,因为当你说“类方法”时,暗示了模拟“classmethod”。方法始终是类实例的一部分,而classmethod是类的一部分。要使其正确,请使用装饰器@classmethod替换方法并添加参数cls。 - msudder
3个回答

72

这里有一个使用模拟的简单方法

import mock


def new_loaddata(cls, *args, **kwargs):
    # Your custom testing override
    return 1


def test_SomeProductionProcess():
    with mock.patch.object(SomeProductionProcess, 'loaddata', new=new_loaddata):
        obj = SomeProductionProcess()
        obj.loaddata()  # This will call your mock method

如果可以的话,我建议使用pytest而不是unittest模块。它可以让你的测试代码更加简洁,并减少很多在编写unittest.TestCase风格测试时需要编写的重复代码。


1
谢谢,那么你提到的方法是根据unittest还是pytest呢? - Vineel
2
mock 可以与两者一起使用。使用 pytestunittest 的区别只是改变了测试函数的外观。我的示例使用 pytest 格式,您只需使用常规函数和内置的 assert 语句即可。请查看 @willnx 的 答案 以获取 unittest 测试的示例。 - Brendan Abel
mock现在是Python标准库的一部分,从Python 3.3版本开始作为unittest.mock可用。 - slavugan
@hello 感谢您的评论,但如果有帮助的话,只需要一个赞就足够了。 - Brendan Abel

19

使用 unittest.mock.Mock 可以轻松模拟一个带有结构化返回值的类方法。

from unittest.mock import Mock

mockObject = SomeProductionProcess
mockObject.loaddata = Mock(return_value=True)

编辑:

如果您想用自定义实现来模拟该方法,您可以创建一个自定义Mock方法对象,并在测试运行时替换原始方法。

def custom_method(*args, **kwargs):
    # do custom implementation

SomeProductionProcess.loaddata = custom_method

我想自定义实现loaddata方法,而不仅仅是返回值。 - Vineel
你可以将loaddata分配给自定义实现。等我一秒钟,我会更新我的答案。 - Mattew Whitt

9
假设你有一个名为 awesome.py 的模块,在其中,你有以下代码:
import time

class SomeProductionProcess(CustomCachedSingleTon):

    def loaddata(self):
        time.sleep(30) # simulating a long running process
        return 2

那么,你的单元测试中模拟 loaddata 的部分可能如下所示:
import unittest

import awesome # your application module


class TestSomeProductionProcess(unittest.TestCase):
    """Example of direct monkey patching"""

    def test_loaddata(self):
        some_prod_proc = awesome.SomeProductionProcess()
        some_prod_proc.loaddata = lambda x: 2 # will return 2 every time called
        output = some_prod_proc.loaddata()
        expected = 2

        self.assertEqual(output, expected)

或者它可能看起来像这样:

或者它可能看起来像这样:

import unittest
from mock import patch

import awesome # your application module

class TestSomeProductionProcess(unittest.TestCase):
    """Example of using the mock.patch function"""

    @patch.object(awesome.SomeProductionProcess, 'loaddata')
    def test_loaddata(self, fake_loaddata):
        fake_loaddata.return_value = 2
        some_prod_proc = awesome.SomeProductionProcess()

        output = some_prod_proc.loaddata()
        expected = 2

        self.assertEqual(output, expected)

现在当你运行测试时,loaddata不会花费30秒钟来运行那些测试用例。

非常感谢您的回复。我想提供一个需要实现的存根,而不是返回2,我该怎么做?可以使用lambda x:2的方式来代替吗? - Vineel
无论您在lambda的:右侧放置什么内容,都将被返回。 - willnx
我完全删除lambda表达式,改为将其分配给自定义函数,如下所示:some_prod_proc = custom_func()。我担心它会完全覆盖函数定义,而不仅仅是在测试环境中。 - Vineel
没错,就像你描述的那样将其绑定到自定义函数!Python 的酷炫之处在于它是动态类型的,因此您可以在运行时 monkey patch 对象(与 Java 不同,在 Java 中,您无法在运行时动态重新分配方法;这就是为什么依赖注入是 Java 单元测试的“首选”解决方案)。只要您不修改源代码,而只是在单元测试中修补对象,您就不必担心为生产覆盖它们。 - willnx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接