Search code examples
pythonmockingpython-mock

Return or break as a side effect in python mock?


I have a multiprocessing application that has workers running in "while True" loops. For testing, I'd like to be able to mock sys.exit() in a way like this:

with mock.patch('sys.exit') as sys_mock:
  sys_mock.side_effect = break

or

with mock.patch('sys.exit') as sys_mock:
  sys_mock.side_effect = return

So I can break out of the loop and complete my test. Neither of these work, but is there a different way to do what I'm trying to accomplish?


Solution

  • You can use an Exception as side_effect to simulate sys.exit behavior without exit from your test.

    side_effect documentation say:

    This can either be a function to be called when the mock is called, an iterable or an exception (class or instance) to be raised.

    So you can not use a statement like break or return but what you want to do is exit from your run cycle and that can be obtained by raise an Exception ... I hope you don't use a wild try-except in your thread's main cycle.

    I wrote a simple example to test it, I used decorator patch syntax and put inline side_effect=Exception that make the test more readable:

    import sys
    import threading
    import unittest
    from unittest.mock import patch
    
    class T(threading.Thread):
        def __init__(self,  *args, **kwargs):
            super(T, self).__init__(*args, **kwargs)
            self._interrupt = threading.Event()
            self.started = threading.Event() #Used to be sure that we test run() behavior 
            self.started.clear()
            self.terminated = False
    
        def interrupt(self):
            self._interrupt.set()
    
        def run(self, *args, **kwargs):
            self._interrupt.clear()
            self.started.set()
            while not self._interrupt.is_set():
                self._interrupt.wait(timeout=1)
            self.terminated = True
            sys.exit()
    
    
    class TestInterrupt(unittest.TestCase):
    
        @patch("sys.exit", side_effect=Exception("Ignore it... just close thread"))
        def test_interrupt(self, mock_sys_exit):
            t = T()
            t.start()
            if not t.started.is_set():
                t.started.wait(timeout=0.2)
            self.assertTrue(t.started.is_set(), "t not started!")
            #Ok t is in run() main cycle: we can test interrupt
            t.interrupt()
            t.join(0.1)
            self.assertTrue(t.terminated)
            self.assertFalse(t.isAlive())