How to do assert on instantiated mock class object

Viewed 5850

In the constructor of my class under test a socket object is instantiated and assigned to a class member. I mocked the socket class and set a mocked socket object as return value to the socket constructor call. I then want to assert that connect() and sendall() is called on that object. I always get the assert error that the functions are not called when I assert on the original mocked class object or the one that I set to return on constructor call.

I know I can’t mock the class that is under test (and its members) because that would defeat the purpose here.

Pseudo code:

import socket

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

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

Test:

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

The purpose of this test is:

  1. ensure the constructor of socket library is called with the right arguments.

  2. ensure that connect() is called with right arguments.

  3. ensure that sendall() is called with exactly what I want it to be called, when I pass message into mySocketHandler.write() method.

2 Answers

You're on the right track, but there are a couple things that need to change for this test to work.

Part of your problem right off the bat is that the mock that patch passes into your test method is called mockSocket, but your test code is referring to something called mock_socket.

Also, patch's first argument, the thing you want to patch, should be a string representation of the path to the module where you want to patch something. If your file structure looks like this:

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

and you run your tests from the root directory, you'd want to call patch like this: @mock.patch("app_directory.socketHandler.socket")

  1. Constructor is called - The most important thing to realize is that mockSocket is a Mock object representing the socket class. So to test that the constructor was called, you need to check mockSocket.assert_called_with(...). That will pass if your production calls socket(...).

    You may also want to assert that mySocketHandler.socket is the same object as mockSocket.return_value, to test that mySocketHandler not only calls the constructor, but assigns it to the right attribute.

  2. and 3. connect and sendall are called properly - You should never call your mock in a test, because it can lead to falsely-passing assertions. In other words, you want your production code to be the only thing calling mocks. This means you shouldn't use the line new_sock = mock_socket(), because then your previous assertion about the constructor will pass no matter what your production code does, and I think it's causing your other assertions to fail.

    mockSocketis already an instance of Mock, so it's return value will automatically be another, different Mock instance. Therefore, you don't need the first 2 lines of your test code above, and you only need one of the assertions on connect. The same ideas apply to sendall.

That's a lot to take in, here's what your test would look like if I wrote it:

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)

Bonus Round! MagicMocks behave like Mocks, except that they implement some default values for some magic methods. I don't use them unless I absolutely need them. Here's an example:

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

The complete answer derived from hints given by @ryanh119 and this post link

I will fix the example given above by ryanh119 and refrain from editing original question which i messed up, so for completeness:

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)

I also did some research and there would have been two more solutions that I want to mention. They are not as pythonicaly correct like the above ones but here it is:

  1. Make use of dependency injection by changing the __init__ of socketHandler to take in a socket object and only instantiate it if not supplied in the args. That way i could have passed in a mock or MagicMock object and used that to do the asserts on.
  2. Make use of a extremely powerful mocking/patching tool called MonkeyPatch which actually can patch/mock instance variables of classes. This approach would have been like trying to kill a fly with a rocket launcher.
Related