【问题标题】:Using Python unittesting libraries (unittest, mock), how to assert if a method of class B was called within a method of class A?使用 Python 单元测试库(unittest、mock),如何断言 B 类的方法是否在 A 类的方法中被调用?
【发布时间】:2018-08-30 16:07:48
【问题描述】:

假设如下设置:

class A:
    def __init__(self, nodes):
        self.nodes=nodes

    def update(self, bool_a=True):
        if bool_a:
            for n in self.nodes:
                if hasattr(self.nodes[n], 'update'):
                    self.nodes[n].update()

class B:
    def __init__(self, int_attr=5):
        self.int_attr=int_attr

    def update(self):
        self.int_attr = 0

让我们假设 A 类中的节点列表实际上是 B 类的实例列表。

如何为A类的update方法编写单元测试,检查A类的self.nodes中包含的每个B类节点的update方法是否被调用?

在更一般的设置中,让我们假设有多个类实现了更新方法,并且可以是类 A 的 self.nodes 中的节点。如何检查 self.nodes 成员的所有更新方法都被调用了?

我尝试了以下方法,但没有成功:

mock_obj = MagicMock()
@patch('module.A.update', return_value=mock_obj)
def test_update(self, mock_obj):
    nodes = {}
    nodes['first'] = B(int_attr=1)
    nodes['second'] = B(int_attr=2)
    test_A = module.A(nodes=nodes)
    test_A.update(bool_A=True)
    self.assertTrue(mock_obj.called)

mocking a function within a class method 建议的那样。

编辑:如果我们假设这种特殊情况:

import unittest
import mock
from unittest import TestCase

class A:
    def __init__(self, nodes):
        self.nodes=nodes

    def update(self, bool_a=True):
        if bool_a:
            to_update = [n for n in self.nodes]
            while len(to_update) > 0:
                if hasattr(self.nodes[to_update[-1]], 'update'):
                    self.nodes[to_update[-1]].update()
                    print('Update called.')
                    if self.nodes[to_update[-1]].is_updated:
                        to_update.pop()

class B:
    def __init__(self, int_attr=5):
        self.int_attr=int_attr
        self.is_updated = False

    def update(self):
        self.int_attr = 0
        self.is_updated = True

class TestEnsemble(TestCase):
    def setUp(self):
        self.b1 = B(1)
        self.b2 = B(2)
        self.b3 = B(3)
        self.nodes = {}
        self.nodes['1'] = self.b1
        self.nodes['2'] = self.b2
        self.nodes['3'] = self.b3
        self.a = A(self.nodes)

    @mock.patch('module.B.update')
    def test_update(self, mock_update):
        mock_update.return_value = None
        self.a.update()
        with self.subTest():
            self.assertEqual(mock_update.call_count, 3)

在这种情况下运行 unittest 会导致无限循环,因为 is_updated 属性永远不会设置为 True,因为 B 类的更新方法是模拟的。在这种情况下,如何测量在 A.update 中调用 B.update 的时间量?

更新: 试过这个:

@mock.patch('dummy_script.B')
def test_update(self, mock_B):
    self.a.update()
    with self.subTest():
        self.assertEqual(mock_B.update.call_count, 3)

update 函数现在确实运行了 3 次(我在控制台输出中看到它,因为“Update called.”被打印了 3 次),但是 update 方法的 call_count 保持为零。我是否检查了错误的属性/对象?

【问题讨论】:

  • 你永远不应该嘲笑你真正想要测试的东西,那是完全倒退的。你为什么不建立一个模拟 B 的列表并传递它呢?另请注意stackoverflow.com/q/1132941/3001761stackoverflow.com/q/240178/3001761,您的测试和生产代码中的可变对象存在重大问题。
  • 好吧,我这里只用了一个列表作为例子,我同意这并不理想。实际上,它无论如何都是字典(也是可变的)。我更新了问题。
  • 然后制作一个mock字典并传入;思路是一样的,这里完全不需要打补丁。
  • 您介意帮我写一个短代码 sn-p 吗?如您所见,我对单元测试很陌生...:/抱歉打扰:)

标签: python python-3.x unit-testing mocking


【解决方案1】:

如何为TestA.test_update() 编写单元测试以查看是否调用了B.update()

这只是提供一些想法。

import mock
import unittest
import A
import B

class TestB(unittest.TestCase):

    # only mock away update method of class B, this is python2 syntax
    @mock.patch.object(B, 'update')
    def test_update(self, mockb_update):
        # B.update() does not return anything
        mockb_update.return_value = None
        nodes = {}
        nodes['first'] = B(int_attr=1)
        nodes['second'] = B(int_attr=2)
        test_A = A(nodes)
        test_A.update(bool_A=True)
        self.assertTrue(mockb_update.called)

我如何检查所有B.update() 是否为所有A.nodes 调用?

    # same everthing except this
    self.assertEqual(mockb_update.call_count, 2)

在 OP 更新代码后,B.is_udpated 未被模拟时陷入无限循环

__init__ 内模拟B.is_updated 或模拟类__init__ 是一个比原帖更复杂的主题

这里有几个想法,B.is_updated不能只是mock.patch,它只有在B类启动后才可用。所以选择是

a) 模拟 B.__init__,或类构造函数

b) 模拟整个班级B,在您的情况下这更容易,将is_updated 设置为True,将结束无限循环。

【讨论】:

  • 这似乎行得通!如果 B.update 会返回一个不平凡的值,有什么区别吗?如果节点包含多个不同类型的类(比如 B、C 和 D),它将如何工作?谢谢!
  • @ElBrutale,mockb_update.return_value 在这里确实有任何区别,默认值为 Mock() 对象。如果节点是不同类型的类,你可以尝试多个补丁,或者补丁A.update()以获得最小的覆盖率,例如方法存在,call_args,called_with等。
  • 非常感谢,这很有帮助。如果你觉得很慷慨,我编辑了这个问题(在模拟方法中更新属性的情况有点扭曲),我会很感激一些建议:)
  • 我编辑了问题,无法在评论中发布代码...再次感谢您的帮助!
【解决方案2】:

结合@Gang 和@jonrsharpe 的回答,下面的代码sn-ps 解决了我上面提出的问题:

如何为 TestA.test_update() 编写单元测试以查看是否调用了 B.update()? 请参阅@Gangs 的回答。

如何检查是否为所有 A.node 调用了所有 B.update()? 请参阅@Gangs 的回答。

在 OP 更新代码后未模拟 B.is_udpated 时陷入无限循环

正如@jonrsharpe 建议的那样,这里的解决方案是在节点中为每个 B 实例创建一个模拟对象,并分别检查函数调用:

class TestA(TestCase):

    @mock.patch('module.B')
    @mock.patch('module.B')
    @mock.patch('module.B')
    def test_update(self, mock_B1, mock_B2, mock_B3):
        nodes = {}
        nodes['1'] = mock_B1
        nodes['2'] = mock_B2
        nodes['3'] = mock_B3
        a = A(nodes)
        a.update()
        with self.subTest():
            self.assertEqual(mock_B1.update.call_count, 1)
        with self.subTest():
            self.assertEqual(mock_B2.update.call_count, 1)
        with self.subTest():
            self.assertEqual(mock_B3.update.call_count, 1)

此外,如果您出于某种原因想要执行模拟函数(以防它们设置了一些影响运行时的标志或变量),可以编写如下测试:

def test_fit_skip_ancestors_all(self):
    nodes = {}
    nodes['1'] = mock_B1
    nodes['2'] = mock_B2
    nodes['3'] = mock_B3
    a = A(nodes)
    with mock.patch.object(A.nodes['1'],'update',wraps=A.nodes['1'].update) as mock_B1, \
mock.patch.object(A.nodes['2'], 'update', wraps=A.nodes['2'].update) as mock_B2, \
mock.patch.object(A.nodes['3'], 'update', wraps=A.nodes['3'].update) as mock_B3:

    a.update()
    with self.subTest():
        self.assertEqual(mock_B1.call_count, 1)
    with self.subTest():
        self.assertEqual(mock_B2.call_count, 1)
    with self.subTest():
        self.assertEqual(mock_B3.call_count, 1)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 2013-03-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多