20

I have been searching stack exchange and around the web for how to do this, but I cannot understand how to mock behaviors for methods. I am trying to mock openpyxl behaviors and behaviors for my custom class. Here is my attempt:

import unittest
from unittest.mock import MagicMock
import openpyxl 
from MyPythonFile import MyClass

class TestMyClass(unittest.TestCase):
  def test_myclass(self):
    myclass = MyClass()
    wb = openpyxl.workbook()
    ws = openpyxl.worksheet()
    wbPath = 'wbPath'

    openpyxl.load_workbook(wbPath, data_only = True) = MagicMock(return_value=wb)

When I try the final line I get the error "can't assign to function call". Do I need to use patch.object('openpyxl','load_workbook')? I am used to mocking in Java with Groovy and it's pretty straightforward.

*Edit: wanted to add the finalized version of the test based on the response from @alxwrd

import unittest
from unittest.mock import MagicMock
import openpyxl 
import configparser
from MyPythonFile import MyClass

class TestMyClass(unittest.TestCase):
  def test_myclass(self):
    myclass = MyClass()
    wb = openpyxl.workbook()
    ws = openpyxl.worksheet()
    config = configparser.ConfigParser()

    openpyxl.load_workbook = MagicMock(return_value=wb)
    wb.get_sheet_by_name = MagicMock(return_value=ws)

    config.sections() = MagicMock(return_value=['Section1'])
    config.get = MagicMock(side_effect=['Value1','Value2']) 

Notice that config.get gives multiple returns with the side_effect parameter, so if config.get() is called once in the code it returns 'Value1' and when config.get() is called a second time it returns 'Value2'.

EliSquared
  • 1,409
  • 5
  • 20
  • 44

2 Answers2

28

You can't override a function call, but you can override the function itself.

From the docs:

>>> from unittest.mock import MagicMock
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')
3
>>> thing.method.assert_called_with(3, 4, 5, key='value')

So in your case:

openpyxl.load_workbook = MagicMock(return_value=wb)
alxwrd
  • 2,320
  • 16
  • 28
  • 1
    Ok, great that works for the loading workbook, but what if I want to mock the same method but with different arguments and have it return different values? For example, if I mock `config = configparser.ConfigParser()` and then I want to mock `config.get('Section1','Value1') = MagicMock(return_value='val1')` and `config.get('Section1','Value2') = MagicMock(return_value='val2')`? – EliSquared May 12 '17 at 15:40
  • You can access what arguments were passed with `.call_args`, and update what is returned with `.return_value`. https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.call_args https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.return_value – alxwrd May 12 '17 at 15:48
  • 3
    Look up side_effect for a MagicMock object. You set it to a function, vs return_value which is set to an explicit value. You can define the function with *args as argument and check it in the function body and return based on the args values. – mauricio777 May 06 '18 at 15:08
  • What if production class is created 2 layers below test_xxx() – uuu777 Mar 05 '20 at 16:03
4

You don't have to import the target you want to mock in your unit tests. Use patch to mock the target. Let's assume your code has this import statement: import openpyxl. Patch then can be used in your test as a decorator:

import unittest
from unittest import mock
from MyPythonFile import MyClass

class TestMyClass(unittest.TestCase):

    @mock.patch('MyPythonFile.openpyxl')
    def test_myclass(self, openpyxl_mock):
        wb_dummy = 'foo'
        openpyxl_mock.load_workbook.return_value = wb_dummy

        myclass = MyClass()
        myclass.load_workbook()  # assuming this calls openpyxl.load_workbook()

Note that you have to add an argument to the test method which will get the mock object.

Or as a context manager:

import unittest
from unittest import mock
from MyPythonFile import MyClass

class TestMyClass(unittest.TestCase):

    def test_myclass(self):
        with mock.patch('MyPythonFile.openpyxl') as openpyxl_mock:
            wb_dummy = 'foo'
            openpyxl_mock.load_workbook.return_value = wb_dummy

            myclass = MyClass()
            myclass.load_workbook()  # assuming this calls openpyxl.load_workbook()
Community
  • 1
  • 1
Milo
  • 655
  • 1
  • 12
  • 25