简体   繁体   中英

Python way to mock a TCP socket for unit testing

My current approach is to mock socket.socket , listen and run my code for one connection, then stop the service.

But socket.socket.accept() is a blocking method.

I tried to simulate socket.socket.accept() by returning a (socket, address) mockup tuple on the first call, then raise a socket.error Exception , so I can break the loop on my app that invokes socket.socket.accept()

Oddly enough all my unit tests pass but py.test process remains active (doing what?? The debugger is not helping: It doesn't stop on any breakpoints after the second call that exits the loop as expected...) hogging resources on an endless loop until it crashes the whole system.

So, what's the right approach here?

TcpService.py :

import socket
import threading

class TcpService:

    _bind_ip = '127.0.0.1'
    _bind_port = 0
    _max_tcp_conn = 20

    def _handle_client_connection(self):
        pass

    def listen_tcp(self):

        self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._server.bind((self._bind_ip, self._tcp_bind_port))
        self._server.listen(self._max_tcp_conn)

        self._tcp_listen = True

        while self._tcp_listen:
            client_sock, address = self._server.accept() # socket.error is supposed to be raised on the second iteration during the unit test, breaking the loop
            client_handler = threading.Thread(
                target=self._handle_client_connection,
                args=(client_sock, address)
            )
            client_handler.start()

test_TcpService.py :

import unittest
import socket
from time import sleep
import mock
import TcpService

def accept_gen():
    for i in range(1):
        mock_socket = mock.MagicMock(name='socket.socket', spec=socket.socket)
        sleep(1)
        yield (mock_socket, ['0.0.0.0', 1234])
    while True:
        sleep(1) # so I have a chance to kill the process before the OS becomes unresponsive
        yield socket.error()



class test_TcpService(unittest.TestCase):

    @mock.patch('socket.socket', autospec=True)
    def test_listen_tcp(self, mock_socket):
        mocked_socket = mock_socket.return_value
        mocked_socket.accept.side_effect = accept_gen()
        sts = TcpService()
        with self.assertRaises(socket.error):
            sts.listen_tcp()
            mock_socket.assert_called_once() # trivial POC check, final test would be more thorough...
        sts.close_tcp()

[EDIT]: The test was OK. The issue was that the serving threads were non-daemonic, so the parent Thread was waiting indefinitely for them to end before shutting down the listening socket. Just setting the client_handler Thread as a daemon before starting it fixes it:

client_handler = threading.Thread(
                target=self._handle_client_connection,
                args=(client_sock, address)
            )
client_handler.setDaemon(True) # There. Fixed.
client_handler.start()

Before realizing that, the best approach I found was: Do not mock the socket and use localhost and port 0 so you can deploy and connect to your server on a random available local port on any environment you end up running your test suite:

Python: unit testing socket-based code?

But, if the socket is not mocked and actually establishing a TCP connection is required, I would argue that it is not a unit test any more, but an integration test, and the results can depend on the sockets library implementation and the underlying platform...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM