Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. How to test with QTest that slot is called?
Forum Updated to NodeBB v4.3 + New Features

How to test with QTest that slot is called?

Scheduled Pinned Locked Moved Solved General and Desktop
14 Posts 2 Posters 1.8k Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • SGaistS Offline
    SGaistS Offline
    SGaist
    Lifetime Qt Champion
    wrote on last edited by
    #2

    Hi,

    Shouldn't you use mouseClick ? A press is not the same thing.

    Interested in AI ? www.idiap.ch
    Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

    A 1 Reply Last reply
    0
    • A Offline
      A Offline
      adamhendry
      wrote on last edited by
      #3

      Hi @SGaist ,

      Yes, it should say mouseClick. I've simplified the test by manually triggering new_action, but still have the same problem:

      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`` action is triggered that ``create_project`` is called.
      
          ``New`` can be triggered either from the menubar or the toolbar.
      
          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
      
          new_action = window.new_action
      
          def check_create_project():
              assert create_project_mock.called
      
          # Act
          new_action.trigger()
      
          # Assert
          qtbot.waitUntil(check_create_project)
      

      I know the function gets called because I added a toy print statement

      myproj/view.py

      def create_project(self):
              """Creates a new project."""
              print('hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii')
      

      and run pytest -s and see it in the output

      stdout sanity check

      ===================================================================================================================== test session starts ======================================================================================================================
      platform win32 -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
      PyQt5 5.15.6 -- Qt runtime 5.15.2 -- Qt compiled 5.15.2
      Using --randomly-seed=1234
      rootdir: %USERPROFILE%\myproj, configfile: pyproject.toml, testpaths: tests
      plugins: hypothesis-6.46.10, cov-3.0.0, doctestplus-0.12.0, env-0.6.2, forked-1.4.0, memprof-0.2.0, mock-3.7.0, qt-4.0.2, randomly-3.12.0, xdist-2.5.0, typeguard-2.13.3
      collected 1 item
      run-last-failure: rerun previous 1 failure (skipped 4 files)
                                                                                                                                                                                                                                                                           
      tests\test_view.py hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii
      F
      

      but the mock still does not get called:

      Error Message

          def check_create_project():
      >       assert create_project_mock.create_project.called
      E       AssertionError: assert False
      E        +  where False = <MagicMock name='View.create_project' id='2173966999264'>.called
      E        +    where <MagicMock name='View.create_project' id='2173966999264'> = <MagicMock name='View' id='2173917178032'>.create_project
      
      1 Reply Last reply
      0
      • SGaistS Offline
        SGaistS Offline
        SGaist
        Lifetime Qt Champion
        wrote on last edited by
        #4

        What does "create_project" do ?

        How do you setup your mock ?

        Interested in AI ? www.idiap.ch
        Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

        1 Reply Last reply
        0
        • A Offline
          A Offline
          adamhendry
          wrote on last edited by
          #5

          What does "create_project" do ?

          I'm doing TDD, so right now nothing. It could do anything. I want to assert that the right function gets called when I trigger my action.

          How do you setup your mock ?

          Using python's built-in unittest.mock.patch method as a decorator:

          @patch.object(myproj.View, 'create_project', autospec=True, spec_set=True)
          def test_make_project(create_project_mock: Any, app: MainApp, qtbot: QtBot):
             ...
          

          I know in python I am supposed to patch where the method is called, not where it is defined. However, I don't know who calls create_project since that seems to be handled by Qt.

          1 Reply Last reply
          0
          • SGaistS Offline
            SGaistS Offline
            SGaist
            Lifetime Qt Champion
            wrote on last edited by
            #6

            Then there's the need to have a bit of architecture done.

            The name of the method suggests that something should be generated. Your test should then check that the stuff was generated correctly.

            As for the mock part, that one depends on how your class is used in your application. Since you are testing not the class itself but its use, you should replace the object where it's created.

            Interested in AI ? www.idiap.ch
            Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

            A 1 Reply Last reply
            0
            • A Offline
              A Offline
              adamhendry
              wrote on last edited by adamhendry
              #7

              Here is the MRE:

              Directory Structure

              myproj/
              ├─myproj/
              │ ├─main.py
              │ ├─view.py
              │ └──__init__.py
              └─tests/
                ├─conftest.py
                ├─test_view.py
                └─__init__.py
              

              The __init__.py files are empty.

              main.py

              """Myproj main application (controller)."""
              import myproj.view as view
              
              class MainApp:
                  def __init__(self) -> None:
                      """Myproj GUI controller."""
                      self.view = view.View(controller=self)
              
                  def show(self) -> None:
                      """Display the main window."""
                      self.view.showMaximized()
              

              view.py

              """Graphic front-end for Myproj GUI."""
              from typing import TYPE_CHECKING
              
              from pyvistaqt import MainWindow  # type: ignore
              from qtpy import QtCore, QtGui, QtWidgets
              
              if TYPE_CHECKING:
                  from emmy.main import MainApp
              
              class View(MainWindow):
              
                  def __init__(
                      self,
                      controller: 'MainApp',
                  ) -> None:
                      """Display GUI main window.
              
                      Args:
                          controller (MainApp): The application controller, in the model-view-controller (MVC)
                              framework sense
                      """
                      super().__init__()
                      self.controller = controller
                      self.setWindowTitle('Myproj')
              
                      self.container = QtWidgets.QFrame()
              
                      self.layout_ = QtWidgets.QVBoxLayout()
              
                      self.container.setLayout(self.layout_)
                      self.setCentralWidget(self.container)
              
                      self._create_actions()
                      self._create_menubar()
                      self._create_toolbar()
              
                  def _create_actions(self) -> None:
                      """Create QAction items for menu- and toolbar."""
                      self.new_action = QtWidgets.QAction(
                          '&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.addToolBar(self.toolbar)
                      self.toolbar.addAction(self.new_action)
              
                  def create_project(self):
                      """Creates a new project."""
                      print('hiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii')
              

              conftest.py

              """Configuration file for pytest tests."""
              from typing import Generator, Sequence, Union
              
              import pytest
              from pytestqt.qtbot import QtBot  # type: ignore
              from qtpy import QtCore
              
              import myproj.main as main
              
              # Register plugins to use in testing
              pytest_plugins: Union[str, Sequence[str]] = [
                  'pytestqt.qtbot',
              ]
              
              
              @pytest.fixture(autouse=True)
              def clear_settings() -> Generator[None, None, None]:
                  """Fixture to clear ``Qt`` settings."""
                  yield
                  QtCore.QSettings().clear()
              
              @pytest.fixture(name='app', scope='function')
              def fixture_app(qtbot: QtBot) -> Generator[main.MainApp, None, None]:
                  """``pytest`` fixture for ``Qt``.
              
                  Args:
                      qtbot (QtBot): pytest fixture for Qt
              
                  Yields:
                      Generator[QtBot, None, None]: Generator that yields QtBot fixtures
                  """
                  # Setup
                  root = main.MainApp()
                  # When in windows mode (non-headless), window must have focus. This is
                  # important so tester does not click away from window and accidentally
                  # fail test.
                  root.view.setWindowFlags(
                      QtCore.Qt.Window | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowStaysOnTopHint
                  )
                  root.show()
              
                  qtbot.addWidget(root.view)
              
                  # Run
                  yield root
              
                  # Teardown - None
              

              test_view.py

              """Tests for ``myproj.view`` module."""
              from typing import Any
              from unittest.mock import patch
              
              import pytest
              from pytestqt.qtbot import QtBot
              from qtpy import QtCore, QtWidgets
              
              import myproj.main as main
              import myproj.view as view
              
              @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`` action is triggered that ``create_project`` is called.
              
                  ``New`` can be triggered either from the menubar or the toolbar.
              
                  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
              
                  new_action = window.new_action
              
                  def check_create_project():
                      assert create_project_mock.called
              
                  # Act
                  new_action.trigger()
              
                  # Assert
                  qtbot.waitUntil(check_create_project)
              
              1 Reply Last reply
              0
              • SGaistS SGaist

                Then there's the need to have a bit of architecture done.

                The name of the method suggests that something should be generated. Your test should then check that the stuff was generated correctly.

                As for the mock part, that one depends on how your class is used in your application. Since you are testing not the class itself but its use, you should replace the object where it's created.

                A Offline
                A Offline
                adamhendry
                wrote on last edited by adamhendry
                #8

                @SGaist

                you should replace the object where it's created.

                I've provided the MRE in the post (I can only post 1x/10 minutes). I believe the object is created in my fixture app in conftest.py. Is that correct? If so, am I patching incorrectly?

                The name of the method suggests that something should be generated

                My question is more general because I want to test create_project separately. Let's assume, e.g., that it's called connect_to_database. I don't want to call that directly and there wouldn't be a GUI aspect to the function. I can test the functionality of connect_to_database in a separate test. The only aspect I want to test now is that the right function gets called when the action is triggered.

                1 Reply Last reply
                0
                • SGaistS Offline
                  SGaistS Offline
                  SGaist
                  Lifetime Qt Champion
                  wrote on last edited by
                  #9

                  Check this article about the integration of mock in pytest.

                  You'll see how to mock a method.

                  You can then set an object variable to true check that the method was called.

                  Interested in AI ? www.idiap.ch
                  Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                  1 Reply Last reply
                  0
                  • A Offline
                    A Offline
                    adamhendry
                    wrote on last edited by
                    #10

                    You can then set an object variable to true check that the method was called.

                    Forgive me, I have read the article, but I am lost. Can you show me how you would make the MRE work?

                    1 Reply Last reply
                    0
                    • A Offline
                      A Offline
                      adamhendry
                      wrote on last edited by adamhendry
                      #11

                      There is an important subtlety I overlooked. The python docs state that you must

                      patch where an object is looked up

                      but it should really be read

                      patch where YOU look up the object

                      I don't call create_project directly in my code (Qt does this under the hood). So, it isn't a good candidate for patching. The rule is:

                      only mock code you own/can change

                      "Growing Object-Oriented Software, Guided by Tests" by Steve Freeman, Nat Pryce

                      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 the 3rd-party implementation changes.

                      Instead, we can use another test-double: a fake. This can be used to override create_project and we can exploit QtCore.QObject.sender() to get information about the caller and assert on it.

                      Lastly, it should be noted that it is easier to manually trigger the action in this test rather than use GUI automation tools like pytest-qt to trigger the action. Instead, you should create a separate test that uses pytest-qt to push the button and assert that the trigger signal is emitted.

                      def test_make_project(app: main.MainApp):
                          """Test when ``New`` action is triggered that ``create_project`` is called.
                      
                          ``New`` can be triggered either from the menubar or the toolbar.
                      
                          Args:
                              app (MainApp): (fixture) The ``PyQt`` main application
                          """
                          class ViewFake(view.View):
                              def create_project(self):
                                  assert self.sender() is self.new_action
                      
                          app.view = ViewFake(controller=app)
                      
                          window = app.view
                          new_action = window.new_action
                      
                          new_action.trigger()
                      
                      1 Reply Last reply
                      0
                      • A Offline
                        A Offline
                        adamhendry
                        wrote on last edited by
                        #12
                        This post is deleted!
                        1 Reply Last reply
                        0
                        • SGaistS SGaist

                          Hi,

                          Shouldn't you use mouseClick ? A press is not the same thing.

                          A Offline
                          A Offline
                          adamhendry
                          wrote on last edited by
                          #13

                          @SGaist said in How to test with QTest that slot is called?:

                          Shouldn't you use mouseClick ? A press is not the same thing.

                          I also want to note that I originally did this for the following reason:

                          By default, QWidgets only receive mouse move events when at least one mouse button is pressed while the mouse is being moved. Using setMouseTracking to True should enable all events, but this does not seem to work in headless mode (i.e. with QT_QPA_PLATFORM=offscreen).

                          Hence, I tried to use mouseMove and mousePress instead.

                          See: https://doc.qt.io/qtforpython-5/PySide2/QtWidgets/QWidget.html#PySide2.QtWidgets.PySide2.QtWidgets.QWidget.setMouseTracking  # pylint: disable=line-too-long
                          

                          Please also note that it appears mousePress and mouseClick in QTest are switched (i.e. mousePress behaves like mouseClick and vice-versa), even when setMouseTracking is True. I experience this behavior, but please reference this SO post for reference.

                          1 Reply Last reply
                          0
                          • SGaistS Offline
                            SGaistS Offline
                            SGaist
                            Lifetime Qt Champion
                            wrote on last edited by
                            #14

                            I haven't had issues triggering slots through button click simulation even when using the offscreen backend. The most mocking I had to do was "answering" QInputMessage dialogs the way the test was validating either the yes or no code paths.

                            Interested in AI ? www.idiap.ch
                            Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

                            1 Reply Last reply
                            0

                            • Login

                            • Login or register to search.
                            • First post
                              Last post
                            0
                            • Categories
                            • Recent
                            • Tags
                            • Popular
                            • Users
                            • Groups
                            • Search
                            • Get Qt Extensions
                            • Unsolved