Search code examples
pythonmockingside-effects

Mocking a class and returning several values


I have the following configuration class:

class ConfigB(object):
  Id = None
  fileName = None

  def __init__(self, file):
    self.Id = self.searchForId(file)
    self.fileName = file

Which is instantiated multiple times in the following class and the properties accessed:

from config.ConfigB import ConfigB

class FileRunner(object):
  def runProcess(self, cfgA)
    for file in cfgA.listFiles:
       cfgB = ConfigB(file)
       print(cfgB.Id)
       print(cfgB.fileName)

To test it I created the following test class where I mock ConfigB for the FileRunner class:

import unittest
import unittest.mock imort MagicMock
import mock
from FileRunner import FileRunner

class TestFileRunner(unittest.TestCase):
  @mock.patch('FileRunner.ConfigB')
  def test_methodscalled(self, cfgB):

    cfgA = Mock()
    cfgA.listFiles = ['File1','File2']

    cfgB().Id.side_effect = [1,2]
    cfgB().fileName.side_effect = ['File1','File2']

    fileRunner = FileRunner()


    fileRunner.runProcess(cfgA)

I am trying to to get the mock for cfgB to return multiple values for both 'Id' and 'fileName'. If I use cfgB().fileName = 'File1', I can get the mock for cfgB to return 'File1' twice, but I would prefer if I could iterate through multiple return values. Is something that can be done?

*Edit: I would like to make clear that the above test does not work for returning specific values and instead I get the following output:

<MagicMock name='cfgB().Id' id='160833320'>
<MagicMock name='cfgB().fileName' id='160833320'>
<MagicMock name='cfgB().Id' id='160833320'>
<MagicMock name='cfgB().fileName' id='160833320'>

Solution

  • The problem here is that you are not actually using side_effect the way it is intended to be used.

    Per the documentation here, the side_effect attribute states:

    A function to be called whenever the Mock is called. See the side_effect attribute. Useful for raising exceptions or dynamically changing return values. The function is called with the same arguments as the mock, and unless it returns DEFAULT, the return value of this function is used as the return value.

    The key thing to realize here is function. The expectation here is something that is actually called. You are actually testing attributes, and attributes are not being called like a function, so you are not actually configuring your test properly with how you are using those side_effect calls.

    Based on what you are looking to test, you should take a slightly different approach. Looking at your code, you are looking to create a ConfigB object inside your loop as you iterate over cfgA.listFiles. So, this indicates that you are actually looking to control the side_effect of what happens when you call ConfigB(file) which you have mock patched as cfgB in your test.

    Furthermore, you are passing what seems like the filenames from iterating over cfgA.listFiles to configB. Therefore, you can just set listFiles as a list of arbitrary filenames as:

    cfgA.listFiles = ['some_file_name_1', 'some_file_name_2']
    

    Then, all you need to do, is then set your cfgB mock's side_effect to now return a Mock object containing the attributes of interest to properly proceed with your testing, as such:

    cfgB.side_effect = [
        Mock(Id="some_id_1", fileName="some_filename_1"),
        Mock(Id="some_id_2", fileName="some_filename_2")
    ]
    

    Running with those modifications, will then yield the following results from your print statements you have in your code:

    some_id_1
    some_filename_1
    some_id_2
    some_filename_2
    

    So, as you can see, now we have successfully set up your iterable to hold the filenames you want to set up for your tests. Furthermore, the side_effect is now properly used on your mock of ConfigB, in order to now return the proper mocked config object holding the attributes that you can test with in each iteration.

    Here is what the final test method looks like all put together:

    class TestFileRunner(unittest.TestCase):
      @mock.patch('FileRunner.ConfigB')
      def test_methodscalled(self, cfgB):
    
        cfgA = Mock()
        cfgA.listFiles = ['some_file_name_1', 'some_file_name_2']
    
        cfgB.side_effect = [
            Mock(Id="some_id_1", fileName="some_filename_1"),
            Mock(Id="some_id_2", fileName="some_filename_2")
        ]
    
        fileRunner = FileRunner()
        fileRunner.runProcess(cfgA)