Search code examples
pythonpytestassertionpython-unittest.mock

How to use unit test's @patch to mock self.attribute.savefig for matplotlib?


I'm trying to figure out how to mock matplotlib's plt.savefig inside a class to test if my GUI's button is actually saving a figure. For simplicity's sake, I will not include the GUI, but only the part I am testing. Here is the code structure:

I'm using pytest to run the test file.

This code is in file1, and it is the code I want to test:

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk


class MyClass:
    
    def __init__(self):
        pass

    def to_test(self):

        if hasattr(self, 'attribute'): # The self.attribute variable would be defined in another function, but I do not want to call that function as part of the test
            filepath= 'the_file_path'
            self.attribute.savefig(filepath)

    def invoke(self): # This is the simplified command the tk.Button of my GUI would call
        self.to_test()

My goal is to make sure that whenever MyClass.invoke() is called, self.attribute.savefig() is also called

This is inside my test file

import unittest
from unittest.mock import patch, MagicMock
import sys
sys.path.append('path to directory')
from file1 import MyClass


@patch('file1.plt.savefig')
def test(mock_save):
    myclass = MyClass()
    myclass.attribute = MagicMock()
    myclass.invoke()
    assert mock_save.call_count == 1

Unfortunately, when I run this test, the following error occurs

E       AssertionError: assert 0 == 1
E        +  where 0 = <MagicMock name='savefig' id='4471947984'>.call_count

Does anyone know why?


Solution

  • The reason why this is happening is that when you assign a MagicMock to .attribute, it has nothing to do with the Mock that your @patch operator has used to override pyplot.savefig. What you can instead do is get rid of the patch and test that the savefig method on the manually-assigned MagicMock is called using assert_called_once. Here's an example from the Python official docs:

    >>> mock = Mock()
    
    >>> mock.method()
    <Mock name='mock.method()' id='...'>
    
    >>> mock.method.assert_called_once()
    
    >>> mock.method()
    <Mock name='mock.method()' id='...'>
    
    >>> mock.method.assert_called_once()
    Traceback (most recent call last):
    ...
    AssertionError: Expected 'method' to have been called once. Called 2 times.
    

    https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_once

    The reason why this happens is that Mock objects create all called methods and attributes as you call them, so when you call attribute.savefig the object creates a new attribute which is itself a callable Mock, and all calls are registered against that.