首页 > 解决方案 > 是否可以修补以前创建的模拟调用的方法?

问题描述

我有在初始化期间创建mocker.Mock而不是对象的夹具。引用存储在属性中。在我的测试中,我检查是否调用了正确的函数。参数化测试运行良好,直到我遇到应该调用方法的条件。现在有一个模拟。gui.Menugui.ButtonsButtons.menugui.Buttons.addgui.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()

使用真实对象,我可以(True, 'project.gui.Menu.show_error')在里面写@pytest.mark.parametrize(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

标签: pythontestingmockingpytest

解决方案


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

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()

推荐阅读