2

I am trying to mock a function that uses multithreading to run another function with different parameters and saves the return results to the queue. I tried using pytest and unitest to mock it but it seems to still execute the thread when called from test function:

from threading import Thread
import threading
import time
import queue
from unittest import mock

def threaded_function(name):
    time.sleep(100)
    return name

def run_threads():
    thread_list = []
    result_list = []
    res_queue = queue.Queue()
    args_list = [("A"), ("B"), ("C")]
    for val in args_list:
        thread = Thread(target=lambda q, arg1: q.put(threaded_function(arg1)), args=(res_queue, val))
        thread.start()
        thread_list.append(thread)
    for thread in thread_list:
        thread.join()
    while not res_queue.empty():
        result_list.append(res_queue.get())
    return result_list

Below are the mock functions which I am trying:

@mock.patch("threading.Thread")
@mock.patch("queue.Queue")
def test_run_threads(mock_queue, mock_thread):
    new_queue = queue.Queue()
    new_queue.put("D")
    mock_queue.return_value = new_queue
    mock_thread.return_value = None
    result = run_threads()
    assert result == ["D"]


class MockThread:
    def __init__(self):
        pass

    def start():
        pass

    def join():
        pass


def test_run_threads2(monkeypatch):
    mock_thread = MockThread()
    monkeypatch.setattr(threading, "Thread", MockThread)
    result = run_threads()
    assert result == []
Incognito
  • 135
  • 4
  • 14

1 Answers1

1

According to Unittest: Where to patch, you need to patch Thread from where it is used (or where it is looked up). In your function run_threads, you are using __main__.Threads instead of threading.Threads because of how from threading import Thread imports. Remove mock_thread.return_value = None and now all your threads in run_threads will be MagicMocks that perform no functionality.

Your next problem is mocking res_queue in run_threads. When patching it in test_run_threads, you have no way of replacing the res_queue with a different queue, because you just replaced all new instances of queue.Queue with MagicMock.

It's better to rewrite this function to be easier to test. I would suggest breaking run_threads() into two functions.

create_thread_list(args_list, res_queue): will be used to create our list of threads. By separating this out, we can change args_list to be whatever list of arguments we want to test.

def create_thread_list(args_list, res_queue):
    thread_list = []

    for val in args_list:
        thread = Thread(target=lambda q, arg1: q.put(threaded_function(arg1)), args=(res_queue, val))
        thread_list.append(thread)

    return thread_list

run_threads_2(thread_list, res_queue): will be used to start the threads.

def run_threads_2(thread_list, res_queue):
    result_list = []

    for th in thread_list:
        th.start()
    for th in thread_list:
        th.join()
    while not res_queue.empty():
        result_list.append((res_queue.get()))
    return result_list

By separating these out, you can pass whatever arguments you want to test for your threads.

Below are some examples of how I would test this now:

import queue
import time
from unittest.mock import patch
class MockThread2:
    def __init__(self, name, result_q):
        self.name = name
        self.result_q = result_q

    def start(self):
        self.result_q.put(self.name)

    def join(self):
        pass

class TestMultiThreadedFunctions(unittest.TestCase):

    def test_run_threads_2(self):
        arg_list = ['A', 'B', 'C']
        result_q = queue.Queue()

        # Testing if created threads actually call the target function
        # without actually calling the function.
        with patch('__main__.threaded_function') as mock_function:
            thread_list = create_thread_list(args_list=arg_list, res_queue=result_q)
            run_threads_2(thread_list=thread_list, res_queue=result_q)

            # Check if all threads ran
            self.assertEqual(len(arg_list), mock_function.call_count)

        arg_list = ['C', 'A', 'D', 'B', 'E']
        result_q = queue.Queue()
        # Using the threaded function, but just patching sleep
        with patch('time.sleep') as mock_sleep:
            thread_list = create_thread_list(args_list=arg_list, res_queue=result_q)
            result_list = run_threads_2(thread_list=thread_list, res_queue=result_q)

            self.assertListEqual(arg_list, result_list)

    def test_run_with_alternate_threads(self):
        # testing with MockThread and expecting nothing in the result_q
        result_q = queue.Queue()
        thread_list = [MockThread() for _ in range(5)]
        expected_list = []
        result_list = run_threads_2(thread_list=thread_list, res_queue=result_q)
        self.assertListEqual(expected_list, result_list)

        # testing with MockThread2
        result_q = queue.Queue()
        thread_list = [MockThread2(str(name), result_q) for name in range(5)]
        expected_list = ['0', '1', '2', '3', '4']
        result_list = run_threads_2(thread_list=thread_list, res_queue=result_q)
        self.assertListEqual(expected_list, result_list)
Phlipi
  • 91
  • 8