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
    #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