Home > Mobile >  PyQt testing failing on testsuite TypeError
PyQt testing failing on testsuite TypeError

Time:06-29

I am currently in the process of writing a testing suite using pytest for a PyQt class I have setup to house a desktop application. I am running into the error below when trying to run the tests. The error renders the tests unable to run at all (i.e. I don't get a success or fail from pytest, the tests simply won't run)

"TypeError: Cannot read properties of null (reading 'testsuites')"

The class I am testing (it renders successfully, so no issue there):

import logging
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon, QAction
from PyQt6.QtWidgets import QMainWindow, QStackedWidget, QToolButton, QToolBar, QWidget, QApplication
import sys


class MainWindow(QMainWindow):
    """

    Main window class - this will house the main window
    for the desktop application.

    """

    def __init__(self) -> None:

        # setup basic page components
        super().__init__()
        self.setWindowTitle("Demo App")
        self.setWindowState(Qt.WindowState.WindowMaximized)

        # call page setup here
        self.setup_application()

    @property
    def logger(self) -> logging.Logger:
        return logging.getLogger(f"{__name__}.{self.__class__.__name__}")

    def setup_application(self) -> None:

        self.logger.debug("Starting MainWindow setup")

        # setup stack of widgets (different views)
        self.setup_stack()

        # setup toolbar for view navigation
        self.setup_toolbar()

    def setup_stack(self) -> None:

        # all views to be added into application
        self.first = QWidget()
        self.second = QWidget()
        self.third = QWidget()
        self.fourth = QWidget()

        # add widgets to stack
        self.stack = QStackedWidget(self)
        self.stack.addWidget(self.first)
        self.stack.addWidget(self.second)
        self.stack.addWidget(self.third)
        self.stack.addWidget(self.fourth)

        # set indexes of views in stack
        self.FIRST_INDEX = 0
        self.SECOND_INDEX = 1
        self.THIRD_INDEX = 2
        self.FOURTH_INDEX = 3

        # finally, set central widget for application
        self.setCentralWidget(self.stack)

    def setup_toolbar(self) -> None:

        # setup actions for views' toolbuttons
        self.first_action = QAction("Page 1", self)
        self.second_action = QAction("Page 2", self)
        self.third_action = QAction("Page 3", self)
        self.fourth_action = QAction("Page 4", self)
        self.first_action.triggered.connect(
            lambda: self.change_window(self.FIRST_INDEX)
        )
        self.second_action.triggered.connect(lambda: self.change_window(self.SECOND_INDEX))
        self.third_action.triggered.connect(
            lambda: self.change_window(self.THIRD_INDEX)
        )
        self.fourth_action.triggered.connect(
            lambda: self.change_window(self.FOURTH_INDEX)
        )

        # setup buttons for toolbar
        self.first_button = QToolButton()
        self.first_button.setDefaultAction(self.first_action)
        self.second_button = QToolButton()
        self.second_button.setDefaultAction(self.second_action)
        self.third_button = QToolButton()
        self.third_button.setDefaultAction(self.third_action)
        self.fourth_button = QToolButton()
        self.fourth_button.setDefaultAction(self.fourth_action)

        # finally, setup toolbar for top of screen
        self.toolbar = QToolBar()
        self.toolbar.addWidget(self.first_button)
        self.toolbar.addWidget(self.second_button)
        self.toolbar.addWidget(self.third_button)
        self.toolbar.addWidget(self.fourth_button)
        self.toolbar.setMovable(False)
        self.addToolBar(self.toolbar)

    def change_window(self, index: int) -> None:

        # TODO: add bold text/button highlighting to make it clear from
        # the toolbar which view you are on

        # set application view to desired view, just doing this for now
        self.stack.setCurrentIndex(index)

Code used to invoke the application normally (non-testing environment); housed in a separate file from the MainWindow class:

def main() -> None:
    # TODO: Figure way to get package name dynamically
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


# invoke application
if __name__ == "__main__":
    main()

My test code as it currently stands (housed in a file called test_main_window.py):

# add imports for any files to be tested in windows
# package imports
import pytest
from pytest_mock import MockerFixture
from pytestqt.plugin import QtBot
import sys

# file imports
from windows.main_window import MainWindow


# tests should start with `test_` and be followed by function name
#    example: test_my_fancy_func()

@pytest.fixture
def mw() -> MainWindow:
    return MainWindow()

class TestMainWindow:

    def test_dummy(self, mw: MainWindow) -> None:
        pass

Using the pytest integration/plugin for VS Code gives me the error mentioned above, and I have not been able to figure out why. Pytest seems to be getting upset with my fixture for some reason but I'm not sure why as the MainWindow class performs just as expected in a normal setting. I have tried defining a QApplication for the test file, as well as using the pytest-qt plugin's QtBot. Neither have been able to resolve my issue, although there is a good chance I might have misused either of those in my attempted fixes. Any help would be much appreciated!

CodePudding user response:

I believe the solution to this is related to the fact that you're trying to instantiate the MainWindow widget without first constructing a QApplication.

pytest-qt lets you avoid having to construct a QApplication for each unit test with qtbot.

In your TestMainWindow class, you should simply add qtbot as an argument to your test function.

class TestMainWindow:

    def test_dummy(self, qtbot, mw: MainWindow) -> None:
        pass

Adding the qtbot fixed the issue for me locally.

  • Related