Cannot Assert Button Calls Method
See original GitHub issueI 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
"""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"medtronic.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
Issue Analytics
- State:
- Created a year ago
- Comments:11 (4 by maintainers)
Top Results From Across the Web
Angular CLI button (click) do not call method - Stack Overflow
I wanted to add a button and assign a listener to it on the click event, I have already googled a lot of...
Read more >.simulate('click') only working when done twice on the same ...
and here is my test: it('calls handleSubmit when Submit button is clicked', () => { let wrapper = shallow(<Contact {...mockProps} ...
Read more >API Call Button Not Functioning On First Click or Two
I am currently working on a simple project for a client that wants a tool to calculate the rates they will charge for...
Read more >Selenium Assertion Examples - Practical Applications In Projects
In this Tutorial, we will Discuss How to use Assertions in Various Real-Time Project Scenarios: To verify if an object is visible (Button, ......
Read more >Component testing scenarios - Angular
To correct the problem, call compileComponents() as explained in the following ... Then you can assert that the quote element displays the expected...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
In python, you have to patch where the object is called due to the nature of python’s import mechanism [1, 2] .
connect
linkstriggered
tocreate_project
, but doesn’t actually call it. Otherwise, we would see a print statement after initializingView
since__init__
calls_create_actions
.Qt
internally calls slot functions when signals get triggered, so we would have to patch where that happens inQt
.This reminded me of an important subtlety I overlooked. The python docs state that you should
but it should really be
Since I don’t call
create_project
directly anywhere in my code, this isn’t a good candidate for patching:NB: You can mock a 3rd-party library method, but only when you call it in your code. Otherwise, the test will be brittle because it will break when their implementation changes.
I agree, except on one point: if the function call is expensive (it shouldn’t be here), creating a fake and patching that might be a good idea. For instance, we could override
create_project
and exploitQtCore.QObject.sender()
see the docsThe above works for me.
Thank you
pytest-qt
developers all for your help! Please feel free to close this issue as it appears to be implementation detail on my end and not directly related topytest-qt
.