【问题标题】:How can I write unit tests against code that uses matplotlib?如何针对使用 matplotlib 的代码编写单元测试?
【发布时间】:2015-03-12 22:37:30
【问题描述】:

我正在开发一个 python (2.7) 程序,该程序会产生许多不同的 matplotlib 图形(数据不是随机的)。我愿意实施一些测试(使用 unittest)以确保生成的数字是正确的。例如,我将预期的图形(数据或图像)存储在某个地方,我运行我的函数并将结果与​​参考进行比较。有没有办法做到这一点?

【问题讨论】:

    标签: python matplotlib python-unittest


    【解决方案1】:

    Matplotlib 有一个 testing infrastructure。例如:

    import numpy as np
    import matplotlib
    from matplotlib.testing.decorators import image_comparison
    import matplotlib.pyplot as plt
    
    @image_comparison(baseline_images=['spines_axes_positions'])
    def test_spines_axes_positions():
        # SF bug 2852168
        fig = plt.figure()
        x = np.linspace(0,2*np.pi,100)
        y = 2*np.sin(x)
        ax = fig.add_subplot(1,1,1)
        ax.set_title('centered spines')
        ax.plot(x,y)
        ax.spines['right'].set_position(('axes',0.1))
        ax.yaxis.set_ticks_position('right')
        ax.spines['top'].set_position(('axes',0.25))
        ax.xaxis.set_ticks_position('top')
        ax.spines['left'].set_color('none')
        ax.spines['bottom'].set_color('none')
    

    来自docs

    第一次运行此测试时,将没有基线图像 比较,所以测试将失败。复制输出图像(在 这种情况下 result_images/test_category/spines_axes_positions.*) 到 源目录中baseline_images树的正确子目录 (在这种情况下是 lib/matplotlib/tests/baseline_images/test_category)。重新运行测试时,它们现在应该通过了。

    【讨论】:

    • 我已经尝试过您的方法,但出现错误。在 matplotlib.testing.decorators 中有一个“import matplotlib.tests”会产生一个 ImportError :没有名为测试的模块。我已经进行了一些研究,并且确实在我的 matplolib 文件中没有测试模块,并且文档对此很少提及。有人知道怎么解决吗?
    • 看起来这种方式已被弃用 "MatplotlibDeprecationWarning: ImageComparisonTest 类在 Matplotlib 3.0 中已弃用,将在 3.2 中删除。@image_comparison(baseline_images=['spines_axes_positions'])"
    【解决方案2】:

    在我的experience 中,图像比较测试最终带来的麻烦多于其价值。如果您想跨多个系统(如 TravisCI)运行持续集成,这种情况尤其如此,这些系统可能具有稍微不同的字体或可用的绘图后端。即使功能完全正常工作,要保持测试通过也可能需要做很多工作。此外,以这种方式进行测试需要将图像保存在您的 git 存储库中,如果您经常更改代码,这会很快导致存储库膨胀。

    我认为更好的方法是 (1) 假设 matplotlib 将实际正确绘制图形,并且 (2) 对绘图函数返回的数据运行数值测试。 (如果您知道在哪里查找,也可以随时在 Axes 对象中找到这些数据。)

    例如,假设你想测试一个像这样的简单函数:

    import numpy as np
    import matplotlib.pyplot as plt
    def plot_square(x, y):
        y_squared = np.square(y)
        return plt.plot(x, y_squared)
    

    你的单元测试可能看起来像

    def test_plot_square1():
        x, y = [0, 1, 2], [0, 1, 2]
        line, = plot_square(x, y)
        x_plot, y_plot = line.get_xydata().T
        np.testing.assert_array_equal(y_plot, np.square(y))
    

    或者,等价地,

    def test_plot_square2():
        f, ax = plt.subplots()
        x, y = [0, 1, 2], [0, 1, 2]
        plot_square(x, y)
        x_plot, y_plot = ax.lines[0].get_xydata().T
        np.testing.assert_array_equal(y_plot, np.square(y))
    

    【讨论】:

    • 使用您的解决方案,您需要返回绘图结果。有没有办法做同样的事情而不必退货?通过“捕捉”无花果?我的绘图功能暂时“返回”无效,如果这是我可以改变的唯一方法,但我不想这样做。
    • 看第二个例子,它展示了如何自省坐标轴对象以找到用于在其中绘制绘图的所有数据。
    • @mwaskom 有针对您明确添加的 mpl.collection 运行测试的经验吗?例如ax.add_collection(PatchCollection(...))?
    • 我使用 plt.scatter 并在 ax.collections[0].get_offsets() 而不是 ax.lines[0].get_xydata() 中找到了我的数据。
    【解决方案3】:

    您还可以使用unittest.mock 模拟matplotlib.pyplot 并检查是否对其进行了带有适当参数的适当调用。假设您在module.py 中有一个plot_data(data) 函数(假设它位于package/src/),您想测试它,它看起来像这样:

    import matplotlib.pyplot as plt
    
    def plot_data(x, y, title):
        plt.figure()
        plt.title(title)
        plt.plot(x, y)
        plt.show()
    

    为了在您的test_module.py 文件中测试此功能,您需要:

    import numpy as np
    
    from unittest import mock
    import package.src.module as my_module  # Specify path to your module.py
    
    
    @mock.patch("%s.my_module.plt" % __name__)
    def test_module(mock_plt):
        x = np.arange(0, 5, 0.1)
        y = np.sin(x)
        my_module.plot_data(x, y, "my title")
    
        # Assert plt.title has been called with expected arg
        mock_plt.title.assert_called_once_with("my title")
    
        # Assert plt.figure got called
        assert mock_plt.figure.called
    

    这会检查是否使用参数 my title 调用了 title 方法,以及在 plt 对象的 plot_data 内调用了 figure 方法。

    更详细的解释:

    @mock.patch("module.plt") 装饰器“修补”在module.py 中导入的plt 模块,并将其作为mock 对象(mock_plt) 作为参数注入test_module。这个模拟对象(以mock_plt 传递)现在可以在我们的测试中使用来记录plot_data(我们正在测试的函数)对plt 所做的所有事情——这是因为@987654347 对plt 的所有调用@ 现在将在我们的模拟对象上创建。

    此外,除了assert_called_once_with,您可能还想使用其他类似的方法,例如assert_not_calledassert_called_once 等。

    【讨论】:

    • 这是我的首选方法。此外,重要的是要注意,如果您的代码解包任何对 plt 的调用,例如fig, ax = plt.subplots(),在测试时您可以手动修补 plt.subplots 以返回与预期一样多的 MagicMocks,例如mock_plt.subplots.return_value = (MagicMock(), MagicMock()) 在测试代码之前。
    猜你喜欢
    • 1970-01-01
    • 2015-09-06
    • 1970-01-01
    • 2015-03-30
    • 2011-10-11
    • 1970-01-01
    • 2020-06-25
    • 2013-06-02
    相关资源
    最近更新 更多