I have a PyQt5 GUI that calls a slot when I press a toolbar button. I know it works because the button itself works when I run the GUI. However, I cannot get my pytest to pass.
I understand that, when patching, I have to patch where the method is called rather than where it is defined. Am I defining my mock incorrectly?
NB: I tried to use python's inspect
module to see if I could get the calling function. The printout was
Calling object: <module>
Module: __main__
which doesn't help because __main__
is not a package and what goes into patch
has to be importable.
MRE
Here is the folder layout:
myproj/
├─myproj/
│ ├─main.py
│ ├─model.py
│ ├─view.py
│ ├─widgets/
│ │ ├─project.py
│ │ └─__init__.py
│ ├─__init__.py
│ └─__version__.py
├─poetry.lock
├─pyproject.toml
├─resources/
│ ├─icons/
│ │ ├─main_16.ico
│ │ ├─new_16.png
│ │ └─__init__.py
│ └─__init__.py
└─tests/
├─conftest.py
├─docs_tests/
│ ├─test_index_page.py
│ └─__init__.py
├─test_view.py
└─__init__.py
Here is the test:
Test
@patch.object(myproj.View, 'create_project', autospec=True, spec_set=True)
def test_make_project(create_project_mock: Any, app: MainApp, qtbot: QtBot):
"""Test when New button clicked that project is created if no project is open.
Args:
create_project_mock (Any): A ``MagicMock`` for ``View._create_project`` method
app (MainApp): (fixture) The ``PyQt`` main application
qtbot (QtBot): (fixture) A bot that imitates user interaction
"""
# Arrange
window = app.view
toolbar = window.toolbar
new_action = window.new_action
new_button = toolbar.widgetForAction(new_action)
qtbot.addWidget(toolbar)
qtbot.addWidget(new_button)
# Act
qtbot.wait(10) # In non-headless mode, give time for previous test to finish
qtbot.mouseMove(new_button)
qtbot.mousePress(new_button, QtCore.Qt.LeftButton)
qtbot.waitSignal(new_button.triggered)
# Assert
assert create_project_mock.called
Here is the relevant project code
main.py
"""Myproj entry point."""
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
import myproj
class MainApp:
def __init__(self) -> None:
"""Myproj GUI controller."""
self.model = myproj.Model(controller=self)
self.view = myproj.View(controller=self)
def __str__(self):
return f'{self.__class__.__name__}'
def __repr__(self):
return f'{self.__class__.__name__}()'
def show(self) -> None:
"""Display the main window."""
self.view.showMaximized()
if __name__ == '__main__':
app = QApplication([])
app.setStyle('fusion') # type: ignore
app.setAttribute(Qt.AA_DontShowIconsInMenus, True) # cSpell:ignore Dont
root = MainApp()
root.show()
app.exec_()
view.py (MRE)
"""Graphic front-end for Myproj GUI."""
import ctypes
import inspect
from importlib.metadata import version
from typing import TYPE_CHECKING, Optional
from pyvistaqt import MainWindow # type: ignore
from qtpy import QtCore, QtGui, QtWidgets
import resources
from myproj.widgets import Project
if TYPE_CHECKING:
from myproj.main import MainApp
class View(MainWindow):
is_project_open: bool = False
project: Optional[Project] = None
def __init__(
self,
controller: 'MainApp',
) -> None:
"""Display Myproj GUI main window.
Args:
controller (): The application controller, in the model-view-controller (MVC)
framework sense
"""
super().__init__()
self.controller = controller
self.setWindowTitle('Myproj')
self.setWindowIcon(QtGui.QIcon(resources.MYPROJ_ICO))
# Set Windows Taskbar Icon
# (https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105) # pylint: disable=line-too-long
app_id = f"mycompany.myproj.{version('myproj')}"
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
self.container = QtWidgets.QFrame()
self.layout_ = QtWidgets.QVBoxLayout()
self.layout_.setSpacing(0)
self.layout_.setContentsMargins(0, 0, 0, 0)
self.container.setLayout(self.layout_)
self.setCentralWidget(self.container)
self._create_actions()
self._create_menubar()
self._create_toolbar()
self._create_statusbar()
def _create_actions(self) -> None:
"""Create QAction items for menu- and toolbar."""
self.new_action = QtWidgets.QAction(
QtGui.QIcon(resources.NEW_ICO),
'&New Project...',
self,
)
self.new_action.setShortcut('Ctrl+N')
self.new_action.setStatusTip('Create a new project...')
self.new_action.triggered.connect(self.create_project)
def _create_menubar(self) -> None:
"""Create the main menubar."""
self.menubar = self.menuBar()
self.file_menu = self.menubar.addMenu('&File')
self.file_menu.addAction(self.new_action)
def _create_toolbar(self) -> None:
"""Create the main toolbar."""
self.toolbar = QtWidgets.QToolBar('Main Toolbar')
self.toolbar.setIconSize(QtCore.QSize(24, 24))
self.addToolBar(self.toolbar)
self.toolbar.addAction(self.new_action)
def _create_statusbar(self) -> None:
"""Create the main status bar."""
self.statusbar = QtWidgets.QStatusBar(self)
self.setStatusBar(self.statusbar)
def create_project(self):
"""Creates a new project."""
frame = inspect.stack()[1]
print(f'Calling object: {frame.function}')
module = inspect.getmodule(frame[0])
print(f'Module: {module.__name__}')
if not self.is_project_open:
self.project = Project(self)
self.is_project_open = True
Result
./tests/test_view.py::test_make_project Failed: [undefined]assert False
+ where False = <function create_project at 0x000001B5CBDA71F0>.called
create_project_mock = <function create_project at 0x000001B5CBDA71F0>
app = MainApp(), qtbot = <pytestqt.qtbot.QtBot object at 0x000001B5CBD19E50>
@patch('myproj.view.View.create_project', autospec=True, spec_set=True)
def test_make_project(create_project_mock: Any, app: MainApp, qtbot: QtBot):
"""Test when New button clicked that project is created if no project is open.
Args:
create_project_mock (Any): A ``MagicMock`` for ``View._create_project`` method
app (MainApp): (fixture) The ``PyQt`` main application
qtbot (QtBot): (fixture) A bot that imitates user interaction
"""
# Arrange
window = app.view
toolbar = window.toolbar
new_action = window.new_action
new_button = toolbar.widgetForAction(new_action)
qtbot.addWidget(toolbar)
qtbot.addWidget(new_button)
# Act
qtbot.wait(10) # In non-headless mode, give time for previous test to finish
qtbot.mouseMove(new_button)
qtbot.mousePress(new_button, QtCore.Qt.LeftButton)
qtbot.waitSignal(new_button.triggered)
# Assert
> assert create_project_mock.called
E assert False
E + where False = <function create_project at 0x000001B5CBDA71F0>.called