9

I have several functions that create plots, which I use in Jupyter notebooks to visualise data.

I want to create basic tests for these, checking that they still run without erroring on various inputs if I make changes. However, if I call these functions using pytest, creating the plots causes the program to hang until I manually minimise the plot.

import pytest 
import matplotlib.pyplot as plt 

def plot_fn():
    plt.plot([1,2,3])
    plt.show()


def test_plot_fn():
   plot_fn()

How can I test that functions like 'plot_fn' run without erroring using Pytest? I tried the following, but it doesn't work, I think because plt.show() causes the script to hang, and so not reach plt.close('all').

def test_plot_fn():
   plot_fn()
   plt.close('all')

I'm happy to change the behaviour of my plotting function, for example to return the plt object?

oli5679
  • 1,709
  • 1
  • 22
  • 34
  • Is this a duplicate of https://stackoverflow.com/questions/27948126/how-can-i-write-unit-tests-against-code-that-uses-matplotlib ? Had you not done any research? It took me less than 5 seconds to find https://matplotlib.org/devel/testing.html. – AMC Feb 08 '20 at 18:44
  • 2
    I googled specifically Pytest I did link to this when I found it. Happy to remove as duplicate but your comment is a bit aggressive? – oli5679 Feb 08 '20 at 19:49
  • 1
    _Happy to remove as duplicate but your comment is a bit aggressive?_ My apologies, I didn't want to come off as that aggressive. – AMC Feb 08 '20 at 19:55
  • 2
    Neither of the links address this question. Both talk about how to check if stuff is plotted **correctly** while this question merely asks if the functions **run without error**. – Joooeey Feb 10 '23 at 10:18

5 Answers5

5

I am not sure how matplotlib interacts with pytest however it seems like you need to use fixtures in order to achieve this. You also need to create some sort of assert statement in your test that will signal for the test to gracefully tear down your fixture. Some thing like this should achieve the desired results.

import pytest 
import matplotlib.pyplot as plt 

@pytest.fixture(scope='function')
def plot_fn():
    def _plot(points):
        plt.plot(points)
        yield plt.show()
        plt.close('all')
    return _plot


def test_plot_fn(plot_fn):
    points = [1, 2, 3]
    plot_fn(points)
    assert True

If you want to simply monkeypatch the show behavior I would do it like shown below.

def test_plot_fn(monkeypatch):
    monkeypatch.setattr(plt, 'show', lambda: None)
    plot_fn()
gold_cy
  • 13,648
  • 3
  • 23
  • 45
4

This works.

from unittest.mock import patch 
import pytest 
import matplotlib.pyplot as plt 

def plot_fn():
    plt.plot([1,2,3])
    plt.show()

@patch("matplotlib.pyplot.show")
def test_plot_fn(mock_show):
    plot_fn()

Based on this answer (possible duplicate) Turn off graphs while running unittests

oli5679
  • 1,709
  • 1
  • 22
  • 34
  • use monkeypatch instead if you are using pytest – gold_cy Feb 08 '20 at 15:00
  • Why monkeypatch, and how would I do that? – oli5679 Feb 08 '20 at 15:14
  • it's a fixture that you pass directly into your test function. you can see how to use it [here](https://docs.pytest.org/en/latest/monkeypatch.html). as for why I would use it, since you're already using `pytest` might as well be consistent and use their fixtures. I went ahead and updated my answer as well to demonstrate how to do that as well. – gold_cy Feb 08 '20 at 18:08
0

I can think about two options:

  1. use the plt.show(block=False) and still close the figure when test ends.

  2. in test instead of plt.show save the plot to a file plt.savefig. This way you can also compare the created file to a test ref and also test that you plot is the same. Of course for this option you'll have to let your function "know" it is in test mode by optional parameter, env variable etc.

Lior Cohen
  • 5,570
  • 2
  • 14
  • 30
  • I see 1 would work but it's not ideal to add plot = True as argument to every function. 2 doesn't work for my Jupyter notebook workflow. – oli5679 Feb 08 '20 at 13:45
  • 1
    you don't have to add plot=true for every function you can use more global solution like module level variable. – Lior Cohen Feb 08 '20 at 13:53
  • 2
    you can also have pytest mock plt.show() to plt.show(block=False) behavior. This way you don't need to change your code at all. – Lior Cohen Feb 08 '20 at 13:57
0

This one-line decorator will patch the plt.show() behavior and suppress the wait/hang part of showing the pyplot figure during a unit test, like this:

import pytest
import unittest
try:
    # python 3.4+ should use builtin unittest.mock not mock package
    from unittest.mock import patch
except ImportError:
    from mock import patch


@patch("methylcheck.qc_plot.plt.show")
def test_plot_fn(mock_this):
   plot_fn()
   plt.close('all')

Note: you have to patch the show() function within the namespace path of the module/package you are testing, so in this case, the methylcheck package has a qc_plot file that imports pyplot in the usual way (import matplotlib.pyplot as plt) and THAT import gets patched.

Marc Maxmeister
  • 4,191
  • 4
  • 40
  • 54
  • While the accepted answer is similar, I wanted to highlight the nuance of making this patch the function actually called in a series of unit tests, when building a real pytest suite of tests. – Marc Maxmeister Feb 08 '22 at 19:03
0

You can use plt.ioff() for this purpose, see https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.isinteractive.html#matplotlib.pyplot.isinteractive:

In interactive mode, pyplot.show will not block by default.

# run as
# pytest test_plotting.py

from matplotlib import pyplot as plt


def plot_fn():
    plt.plot([1,2,3])
    plt.show()
    assert False # to check that the code gets here

def test_plot_fn():
    with plt.ion():
        plot_fn()
Maximilian Mordig
  • 1,333
  • 1
  • 12
  • 16