模拟在被测试方法中实例化的Python类

3 投票
1 回答
8683 浏览
提问于 2025-04-20 01:18

我有一个正在测试的系统(下面的 Printer 类),它使用了另一个类(下面的 ContentContainer 类)。在一个方法(下面的 retrieve_and_show_content 方法)中,这个类被实例化。在这个方法的测试中(下面的 test_printer_03 方法),我想用一个模拟对象来代替真实的类。但是这样做并不奏效。

我在这里看到,我应该用另一个对象来替换一个 名称 指向的对象。看起来我想替换的对象的名称就是 ContentContainer,而我实际上要替换的对象的名称是 TestMockClass.ContentContainer。这个观察是正确的吗?如果是的话,我该怎么改变这个?如果我在补丁语句中简单地去掉前缀 TestMockClass,我会得到一个 TypeError: Need a valid target to patch. You supplied: 'ContentContainer' 的错误。

#TestMockClass.py
import unittest
from mock import Mock, patch

class Printer():
    def __init__(self, name, cc):
        self.name = name
        self.cc = cc
    def show_content(self):
        text = '{0} says: {1}'.format(self.name, self.cc.content())
        return text
    def retrieve_and_show_content(self):
        cc_tmp = ContentContainer()
        text = '{0} says: {1}'.format(self.name, cc_tmp.content())
        return text

class ContentContainer():
    def __init__(self):
        self.method_counter()
    def content(self):
        return 'Content from ContentContainer'
    def method_counter(self):
        pass

class Test(unittest.TestCase):

    '''No mocking'''
    def test_printer_01(self):
        cc = ContentContainer()
        sut = Printer('P01', cc)
        result = sut.show_content()
        expected_result = 'P01 says: Content from ContentContainer'
        self.assertEqual(result, expected_result, 
                         msg = '\nRetrieved:\n{0} \nExpected:\n{1}'.format(result, expected_result))
        result = sut.retrieve_and_show_content()
        expected_result = 'P01 says: Content from ContentContainer'
        self.assertEqual(result, expected_result, 
                         msg = '\nRetrieved:\n{0} \nExpected:\n{1}'.format(result, expected_result))

    '''Create a mock object, which is the input of the method under test'''
    def test_printer_02(self):
        mock_cc = Mock()
        mock_cc.content.return_value = 'Mocked content'
        sut = Printer('P02', mock_cc)
        result = sut.show_content()
        expected_result = 'P02 says: Mocked content'
        self.assertEqual(result, expected_result, 
                         msg = '\nRetrieved:\n{0} \nExpected:\n{1}'.format(result, expected_result))
        self.assertFalse(mock_cc.method_counter.called, 'Method method_counter shall not be called')

    '''Create a mock class, which is instantiated inside the method under test'''
    @patch('TestMockClass.ContentContainer')
    def test_printer_03(self, mock_cc):
        mock_cc.content.return_value = 'Mocked content'
        sut = Printer('P03', mock_cc)
        result = sut.retrieve_and_show_content()
        expected_result = 'P03 says: Mocked content'
        self.assertEqual(result, expected_result, 
                         msg = '\nRetrieved:\n{0} \nExpected:\n{1}'.format(result, expected_result))
        self.assertFalse(mock_cc.method_counter.called, 'Method method_counter shall not be called')

if __name__ == "__main__":
    unittest.main()

当这个单元测试运行时,输出是:

AssertionError: 
Retrieved:
P03 says: Content from ContentContainer 
Expected:
P03 says: Mocked content

1 个回答

14

有两件事:

  1. 因为 ContentContainer 现在和测试在同一个文件里,所以你需要修补 __main__.ContentContainer

    @patch('__main__.ContentContainer')
    
  2. 因为 ContentContainer 是一个类,而你是在这个类的一个实例上调用 content,所以你实际上是想在这个实例上模拟 content,而不是在类上。因此,你需要这样做: mock_cc.return_value.content.return_value = 'Mocked content'(注意这里多了一个 .return_value,是为了确保你是在模拟实例,而不是类)。这是因为调用一个类会创建一个实例。所以,实例就是对这个类的调用的返回值。

所以测试应该看起来像:

@patch('__main__.ContentContainer')
def test_printer_03(self, mock_cc):
    mock_cc.return_value.content.return_value = 'Mocked content'
    ...

撰写回答