【问题标题】:Is it possible to patch method called on created previously mock?是否可以修补以前创建的模拟调用的方法?
【发布时间】:2020-05-07 17:26:11
【问题描述】:

我有在gui.Buttons 初始化期间创建mocker.Mock 而不是gui.Menu 对象的夹具。引用存储在Buttons.menu 属性中。在我的测试中,我检查是否在gui.Buttons.add 中调用了正确的函数。参数化测试运行良好,直到我遇到应该调用来自gui.Menu 的方法的条件。现在有一个模拟。

import pytest
from project import gui

@pytest.fixture
def buttons(mocker):
    mocker.patch('project.gui.tk.Frame.__init__', return_value=None)
    mocker.patch('project.gui.tk.Button')
    return gui.Buttons(mocker.Mock())


@pytest.mark.parametrize('value,patched',(
        (None, 'project.gui.show_no_connection'),
        (False, 'project.gui.Buttons.process_data'),
        (True, 'pytest.Mock.show_error'),
))
def test_add_calls_function(buttons, value, patched, mocker):
    mocker.patch('project.gui.Buttons.exist_check', return_value=value)
    mocked = mocker.patch(patched)
    buttons.add()
    mocked.assert_called_once()

对于真实对象,我可以在 @pytest.mark.parametrize 中写入 (True, 'project.gui.Menu.show_error') 而不是 (True, 'pytest.Mock.show_error'),后者不起作用并产生 ModuleNotFoundError: No module named 'pytest.Mock'

我想知道是否可以 patch 在我的夹具中创建模拟对象以使其像其他参数化示例一样工作。甚至可能吗?如果我的理解是错误的,请纠正我。

测试代码如下:

import tkinter as tk
import tkinter.messagebox as msg

from project.connection import Database


def show_no_connection():
    msg.showerror('Error', 'Could not perform operation. Try again later.')


class Menu(tk.Tk):

    def __init__(self):
        super().__init__()
        self.form = Form()

    def show_error(self, message):
        self.form.clear()
        msg.showerror('Error', message)


class Form(tk.Frame):

    def clear(self):
        print('Clearing...')

    def get(self):
        return {'Title': 'Test', 'ISBN': 87327837823}


class Buttons(tk.Frame):

    def __init__(self, menu):
        super().__init__(menu)
        self.menu = menu

    def process_data(self, data, operation):
        operation(data)

    def add(self):
        data = self.menu.form.get()
        exists = self.exist_check(data.get('ISBN', None))
        if exists is None:
            show_no_connection()
        else:
            if exists:
                self.menu.show_error(
                    'Record with set ISBN already exists in database.')
            else:
                self.process_data(data, Database().add)

    @staticmethod
    def exist_check(number):
        if number:
            return Database().search({'ISBN': number})
        return False

显示错误:

=================================== FAILURES ===================================
_________ test_add_calls_function[True-project.gui.Gui.show_error] _________

buttons = <[AttributeError("'Buttons' object has no attribute '_w'") raised in repr()] Buttons object at 0x7f840114aa10>
value = True, patched = 'project.gui.Gui.show_error'
mocker = <pytest_mock.plugin.MockFixture object at 0x7f840114ab90>

    @pytest.mark.parametrize('value,patched',(
            (None, 'project.gui.show_no_connection'),
            (False, 'project.gui.Buttons.process_data'),
            (True, 'project.gui.Gui.show_error'),
    ))
    def test_add_calls_function(buttons, value, patched, mocker):
        mocker.patch('project.gui.Buttons.exist_check', return_value=value)
        mocked = mocker.patch(patched)
        buttons.add()
>       mocked.assert_called_once()
E       AssertionError: Expected 'show_error' to have been called once. Called 0 times.

tests/test_gui_buttons.py:88: AssertionError

【问题讨论】:

  • 为什么不能只使用(True, 'project.gui.Menu.show_error')project.gui.Menu 现在指向模拟,所以它应该可以工作。
  • @MrBean-Bremen 这是我所期望的,但它不起作用。我得到AssertionError: Expected 'show_error' to have been called once. Called 0 times.
  • 啊,好的 - 您在使用的测试模块中有一个 gui 的本地副本。请改用import projectproject.gui.Buttons。检查where to patch
  • @MrBean-Bremen 我试过了,还是出现同样的错误。
  • @MrBean-Bremen 我添加了模仿我的代码结构的示例并粘贴了测试失败的完整信息。

标签: python testing mocking pytest


【解决方案1】:

我看不出有可能在同一个测试中处理这个问题 - 您可能需要对最后一次调用进行单独的测试。问题是菜单已被模拟,您需要特定的菜单模拟来测试函数调用(将从该模拟实例调用该函数)。
这是一个可能的工作实现:

import pytest

# don't use "from project import gui" here to not make a copy in the test module
# that would be used instead of the mocked one
import project.gui  

@pytest.fixture
def menu_mock(mocker):
    # gives the possibility to access the menu mock
    # we need the return_value to get the instance instead of the class
    return mocker.patch('project.gui.Menu').return_value

@pytest.fixture
def buttons(mocker, menu_mock):
    mocker.patch('project.gui.tk.Frame.__init__', return_value=None)
    mocker.patch('project.gui.tk.Button')
    return project.gui.Buttons(menu_mock)


@pytest.mark.parametrize('value, patched',(
        (None, 'project.gui.show_no_connection'),
        (False, 'project.gui.Buttons.process_data')
))
def test_add_calls_function(buttons, value, patched, mocker):
    # unchanged except for the missing parametrize case
    mocker.patch('project.gui.Buttons.exist_check', return_value=value)
    mocked = mocker.patch(patched)
    buttons.add()
    mocked.assert_called_once()


def test_add_calls_show_error(buttons, menu_mock, mocker):
    mocker.patch('project.gui.Buttons.exist_check', return_value=True)
    buttons.add()
    # you now have access to the mocked menu instance
    menu_mock.show_error.assert_called_once()

【讨论】:

  • 感谢您对此的解释。我希望把这个案例放在那个参数化的测试中。现在我明白了,这是不可能的。
猜你喜欢
  • 1970-01-01
  • 2014-02-15
  • 2020-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-10
  • 2019-05-22
相关资源
最近更新 更多