30

I am new to Python, so I apologize if this is a duplicate or overly simple question. I have written a coordinator class that calls two other classes that use the kafka-python library to send/read data from Kafka. I want to write a unit test for my coordinator class but I'm having trouble figuring out how to best to go about this. I was hoping that I could make an alternate constructor that I could pass my mocked objects into, but this doesn't seem to be working as I get an error that test_mycoordinator cannot be resolved. Am I going about testing this class the wrong way? Is there a pythonic way I should be testing it?

Here is what my test class looks like so far:

import unittest
from mock import Mock
from mypackage import mycoordinator

class MyTest(unittest.TestCase):

    def setUpModule(self):
        # Create a mock producer
        producer_attributes = ['__init__', 'run', 'stop']
        mock_producer = Mock(name='Producer', spec=producer_attributes)

        # Create a mock consumer
        consumer_attributes = ['__init__', 'run', 'stop']
        data_out = [{u'dataObjectID': u'test1'},
                    {u'dataObjectID': u'test2'},
                    {u'dataObjectID': u'test3'}]
        mock_consumer = Mock(
            name='Consumer', spec=consumer_attributes, return_value=data_out)

        self.coor = mycoordinator.test_mycoordinator(mock_producer, mock_consumer)

    def test_send_data(self):
        # Create some data and send it to the producer
        count = 0
        while count < 3:
            count += 1
            testName = 'test' + str(count)
            self.coor.sendData(testName , None)

And here is the class I am trying to test:

class MyCoordinator():
    def __init__(self):
        # Process Command Line Arguments using argparse  
        ...

        # Initialize the producer and the consumer
        self.myproducer = producer.Producer(self.servers,
                                            self.producer_topic_name)

        self.myconsumer = consumer.Consumer(self.servers,
                                            self.consumer_topic_name)

    # Constructor used for testing -- DOES NOT WORK
    @classmethod
    def test_mycoordinator(cls, mock_producer, mock_consumer):
        cls.myproducer = mock_producer
        cls.myconsumer = mock_consumer

    # Send the data to the producer
    def sendData(self, data, key):
        self.myproducer.run(data, key)

    # Receive data from the consumer
    def getData(self):
        data = self.myconsumer.run()
        return data
jencoston
  • 1,262
  • 7
  • 19
  • 35

1 Answers1

47

There is no need to provide a separate constructor. Mocking patches your code to replace objects with mocks. Just use the mock.patch() decorator on your test methods; it'll pass in references to the generated mock objects.

Both producer.Producer() and consumer.Consumer() are then mocked out before you create the instance:

import mock

class MyTest(unittest.TestCase):
    @mock.patch('producer.Producer', autospec=True)
    @mock.patch('consumer.Consumer', autospec=True)
    def test_send_data(self, mock_consumer, mock_producer):
        # configure the consumer instance run method
        consumer_instance = mock_consumer.return_value
        consumer_instance.run.return_value = [
            {u'dataObjectID': u'test1'},
            {u'dataObjectID': u'test2'},
            {u'dataObjectID': u'test3'}]

        coor = MyCoordinator()
        # Create some data and send it to the producer
        for count in range(3):
            coor.sendData('test{}'.format(count) , None)

        # Now verify that the mocks have been called correctly
        mock_producer.assert_has_calls([
            mock.Call('test1', None),
            mock.Call('test2', None),
            mock.Call('test3', None)])

So the moment test_send_data is called, the mock.patch() code replaces the producer.Producer reference with a mock object. Your MyCoordinator class then uses those mock objects rather than the real code. calling producer.Producer() returns a new mock object (the same object that mock_producer.return_value references), etc.

I've made the assumption that producer and consumer are top-level module names. If they are not, provide the full import path. From the mock.patch() documentation:

target should be a string in the form 'package.module.ClassName'. The target is imported and the specified object replaced with the new object, so the target must be importable from the environment you are calling patch() from. The target is imported when the decorated function is executed, not at decoration time.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • How does @mock.patch('consumer.Consumer', autospec=True) mock the consumer class I wrote? More specifically how does it know what I want mocked? I'm seeing an error when I try and run the test (ImportError: No module named consumer). My class Consumer is in consumer.py which is in mypackage. So do I need to specify the full path to the Consumer class in the patch somehow? – jencoston Apr 11 '17 at 19:35
  • @jencoston: You didn't include where `consumer` came from in your code; I made the assumption that that was the name of the module. If `consumer` is inside a package, provide the full name of the import: `mock.patch('package.consumer.Consumer')`. `mock.patch` will import the module and patch the name (last part of the path). – Martijn Pieters Apr 11 '17 at 21:03
  • @MartijnPieters would you please comment about a gotcha: I believe that the class usage must match the mock *exactly*. The code here uses `myconsumer = consumer.Consumer()` and the mock has `consumer.Consumer` so that's a match. In my code I tried instead something like `from myconsumer import Consumer` and `myconsumer = Consumer()` which does *not* match `consumer.Consumer` in the mock. I don't have the experience with `unittest.mock` to explain why. Thx in adv! – chrisinmtown Sep 23 '22 at 12:31
  • @chrisinmtown: see the [*Where to patch* section](https://docs.python.org/3/library/unittest.mock.html#where-to-patch); you need to patch `yourmodule.Consumer` in that case. – Martijn Pieters Oct 07 '22 at 11:12