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

How to do assert on instantiated mock class object

在我的 class 测试中的构造函数中,套接字对象被实例化并分配给 class 成员。我模拟了套接字 class 并将模拟套接字对象设置为 return 值以调用套接字构造函数。然后我想断言在该对象上调用了 connect() 和 sendall() 。当我在原始模拟 class 对象或我在构造函数调用时设置为 return 的对象上断言时,我总是得到断言错误,即函数没有被调用。

我知道我不能嘲笑正在测试的 class(及其成员),因为那样会破坏这里的目的。

伪代码:

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. 确保使用正确的参数调用套接字库的构造函数。

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

  3. 确保当我将消息传递到 mySocketHandler.write() 方法时,调用 sendall() 的方式正是我希望调用的方式。

您走在正确的轨道上,但需要更改一些内容才能使此测试生效。

您的问题的一部分是 patch 传递到您的测试方法的模拟称为 mockSocket,但您的测试代码指的是称为 mock_socket 的东西.

此外,patch 的第一个参数,即您要修补的内容,应该是您要修补的模块路径的 字符串表示形式 某物。如果您的文件结构如下所示:

|-- root_directory
|   |
|   |-- app_directory
|   |   |-- socketHandler.py
|   |   `-- somethingElse.py
|   |
|   `-- test_directory
|       |-- testSocketHandler.py
|       `-- testSomethingElse.py

而你运行你的测试来自根目录,你想像这样调用补丁:@mock.patch("app_directory.socketHandler.socket")

  1. 构造函数被调用 - 最重要的是要意识到mockSocket是一个Mock对象代表套接字class。所以要测试调用了构造函数,您需要检查 mockSocket.assert_called_with(...)。如果您的产品调用 socket(...).

    ,那将通过

    您可能还想断言 mySocketHandler.socketmockSocket.return_value 是同一个对象,以测试 mySocketHandler 不仅调用构造函数,而且将其分配给正确的属性。

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

    mockSocket 已经是 Mock 的实例,因此它的 return 值将自动成为另一个不同的 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)

奖励回合! MagicMocks 的行为类似于 Mocks,只是它们为 some magic methods 实现了一些默认值。除非我绝对需要它们,否则我不会使用它们。这是一个例子:

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

根据@ryanh119 和此 post

给出的提示得出的完整答案

我将修复上面由 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)

我也做了一些研究,我想提一下另外两个解决方案。它们不像上面的那样 pythonically 正确,但这里是:

  1. 通过更改 socketHandler 的 __init__ 以接收套接字对象并仅在未在 args 中提供时实例化它来使用依赖注入。这样我就可以传入模拟或 MagicMock 对象并使用它来进行断言。
  2. 使用名为 MonkeyPatch 的极其强大的 mocking/patching 工具,它实际上可以 patch/mock 类 的实例变量。这种方法就像试图用火箭发射器杀死苍蝇一样。