如何对已实例化的模拟类对象进行断言

3
在我的被测试类的构造函数中,实例化一个 socket 对象并将其分配给一个类成员。我模拟了 socket 类,并将一个模拟的 socket 对象设置为作为 socket 构造函数调用的返回值。然后我想断言该对象上调用了 connect() 和 sendall() 函数。无论我在原始的模拟类对象或在构造函数调用时设置的对象上进行断言,我总是得到断言错误,指出这些函数没有被调用。
我知道我不能模拟被测试类(及其成员),因为那样会破坏测试的目的。
伪代码:
import socket

Class socketHandler():
    def __init__(...):
    self.mySocket = socket(...)
    ...
    self.mySocket.connect(...)

    def write(message):
        self.mySocket.sendall(message)

测试:

from unittest import mock
from unittest.mock import MagicMock #not sure if i need this
import pytest
import socketHandler

@mock.patch(socketHandler.socket)
def test_socket_handler(mockSocket):
    ...
    new_sock = mock_socket()
    mock_socket.return_value = new_sock

    mySocketHandler = SocketHandler(...)

    mock_socket.socket.assert_called_with(...)
    new_sock.connect.assert_called_with(...) #fails (never called)
    mock_socket.connect.assert_called_with(...) #fails (never called)
    #likewise for the sendall() method call when mysocketHandler.write(..)
    #is called

这个测试的目的是:
  1. 确保正确调用socket库的构造函数。

  2. 确保使用正确的参数调用connect()。

  3. 确保在将消息传递给mySocketHandler.write()方法时,sendall()被准确地调用。

2个回答

2
您已经走上了正确的道路,但是有几个问题需要解决才能使这个测试正常工作。
首先,您的问题之一是patch传递给测试方法的模拟对象被称为mockSocket,但您的测试代码引用了一个名为mock_socket的对象。
其次,patch的第一个参数应该是要修补的模块路径的字符串表示形式。如果您的文件结构如下所示:
|-- root_directory
|   |
|   |-- app_directory
|   |   |-- socketHandler.py
|   |   `-- somethingElse.py
|   |
|   `-- test_directory
|       |-- testSocketHandler.py
|       `-- testSomethingElse.py

如果你从根目录运行测试,你需要像这样调用patch:@mock.patch("app_directory.socketHandler.socket")

  1. 构造函数被调用 - 最重要的是要意识到mockSocket是一个表示socket类的Mock对象。因此,要测试构造函数是否被调用,你需要检查mockSocket.assert_called_with(...)。如果你的生产代码调用了socket(...),这个测试就会通过。

    你可能还想断言mySocketHandler.socket是与mockSocket.return_value相同的对象,以测试mySocketHandler不仅调用了构造函数,而且将其分配给了正确的属性。

  2. 和3. connectsendall被正确调用 - 你不应该在测试中调用你的mock,因为这可能导致虚假的通过断言。换句话说,你希望你的生产代码是唯一调用mocks的东西。这意味着你不应该使用new_sock = mock_socket()这一行,因为这样你之前关于构造函数的断言将无论你的生产代码做什么都会通过,我认为它导致了其他断言失败。

    mockSocket已经是Mock的一个实例,因此它的返回值将自动是另一个不同的Mock实例。因此,你不需要上面测试代码中的前两行,而且你只需要在connect上进行一个断言。对于sendall也是同样的道理。

这是一堆需要注意的事情,如果我写了这个测试,它会是这个样子:

from unittest import mock, TestCase
import pytest
import socketHandler

class TestSocketHandler(TestCase):
    @mock.patch("app_directory.socketHandler.socket")
    def test_socket_handler(mockSocketClass): # renamed this variable to clarify that it's a mock of a class.

        # mockSocketClass is already a mock, so we can call production right away.
        mySocketHandler = SocketHandler(...)

        # Constructor of mockSocketClass was called
        mockSocketClass.assert_called_with(...)

        # Instance of mockSocketClass was assigned to correct attribute on SocketHandler
        self.assertIs(mockSocketClass.return_value, mySocketHandler.socket)

        # Production called connect on the return_value of the mock module, i.e. the instance of socket.
        mockSocketClass.return_value.connect.assert_called_with(...)

        # If SocketHandler's constructor calls sendall:
        mockSocketClass.return_value.sendall.assert_called_with(expectedMessage)

奖励环节!MagicMock类似于Mock,但它们为某些魔法方法实现了一些默认值。除非我绝对需要它们,否则我不使用它们。以下是一个示例:

from mock import Mock, MagicMock

mock = Mock()
magic_mock = MagicMock()

int(mock)
>>>Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string or a number, not 'Mock'

len(mock)
>>>Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'Mock' has no len()

int(magic_mock)
>>> 1

len(magic_mock)
>>> 0

谢谢你的回答。虽然不完全正确,但它帮助我找到了正确的答案。对于我的语法错误,我很抱歉。 - J Selecta
  1. 你定义了一个方法,所以不能使用对self的引用,assertIs是unittest.TestCase类中的一个方法!
  2. 当我尝试你的代码时,mockSocketClass.return_value.connect.assert_called_with(...)从未被调用过。为什么不使用MagicMock呢?它们的autospec或spec功能使它们非常有用。
- J Selecta
抱歉,我以为你正在使用unittest.TestCase并从代码中省略了它。您也可以使用Mock的spec。我避免使用它们,因为我很少需要为自己定义任何魔术方法,并且我不希望我的测试在我忘记在MagicMock上覆盖魔术方法时通过。 - lortimer

2
从@ryanh119和这篇文章的提示中得出的完整答案 link
我将修复ryanh119上面给出的示例,并避免编辑原始问题,因此为了完整起见:
from unittest import mock
import pytest
import socketHandler

@mock.patch("app_directory.socketHandler.socket")
def test_socket_handler(mockSocketClass):

# mockSocketClass is already a mock, so we can call production right away.
mySocketHandler = SocketHandler(...)

# Constructor of mockSocketClass was called, since the class was imported
#like: import socket we need to:
mockSocketClass.socket.assert_called_with(...)

# Production called connect on the class instance variable
# which is a mock so we can check it directly.
# so lets just access the instance variable sock
mySocketHandler.mySocket.connect.assert_called_with(...)

# The same goes for the sendall call:
mySocketHandler.mySocket.sendall.assert_called_with(expectedMessage)

我还做了一些研究,还有两个解决方案值得一提。它们不像上面那个那么符合 Python 的规范,但是在这里介绍一下:

  1. 通过更改 socketHandler 的 __init__,使用依赖注入,并且只有在参数中没有传入时才实例化。这样,我就可以传入一个模拟对象或 MagicMock 对象,并使用它来进行断言。
  2. 使用一个非常强大的模拟/补丁工具,称为MonkeyPatch,实际上可以修补/模拟类的实例变量。这种方法就像用火箭发射器打死一只苍蝇。

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