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.9k 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.
  • A Offline
    A Offline
    adamhendry
    wrote on 2 Jun 2022, 19:14 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
    • S Offline
      S Offline
      SGaist
      Lifetime Qt Champion
      wrote on 2 Jun 2022, 19:24 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 2 Jun 2022, 19:47
      0
      • A Offline
        A Offline
        adamhendry
        wrote on 2 Jun 2022, 19:31 last edited by adamhendry 6 May 2022, 22:56
        #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
        • S SGaist
          2 Jun 2022, 19:24

          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 2 Jun 2022, 19:47 last edited by adamhendry 6 Feb 2022, 19:47
          #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
          • S Offline
            S Offline
            SGaist
            Lifetime Qt Champion
            wrote on 2 Jun 2022, 19:55 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 2 Jun 2022, 20:25 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 4 Jun 2022, 07:37 last edited by adamhendry 6 Apr 2022, 17:37
                #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 4 Jun 2022, 17:36 last edited by
                  #12
                  This post is deleted!
                  1 Reply Last reply
                  0
                  • S SGaist
                    2 Jun 2022, 18:15

                    Hi,

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

                    A Offline
                    A Offline
                    adamhendry
                    wrote on 4 Jun 2022, 23:00 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
                    • S Offline
                      S Offline
                      SGaist
                      Lifetime Qt Champion
                      wrote on 5 Jun 2022, 18:25 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

                      14/14

                      5 Jun 2022, 18:25

                      • Login

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