Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

behaviour with QMessageBox.question and other modal dialogs #18

Closed
baudren opened this issue Sep 15, 2014 · 3 comments
Closed

behaviour with QMessageBox.question and other modal dialogs #18

baudren opened this issue Sep 15, 2014 · 3 comments

Comments

@baudren
Copy link

baudren commented Sep 15, 2014

I do not understand how to successfully test the behaviour of modal dialogs, and did not find an example in the documentation -- or anywhere else, for that matter.

Question

How to prevent modal dialogs from poping up in testing, and how do I simulate clicking on their buttons with qtbot?

Sample file

from PySide.QtGui import QMessageBox, QFrame

class Simple(QFrame):
    def query(self):
        self.question = QMessageBox.question(
            self, 'Message', 'Are you sure?',
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No)
        if self.question == QMessageBox.Yes:
            self.answer = True
        else:
            self.answer = False

def test_Qt(qtbot):
    simple = Simple()
    qtbot.addWidget(simple)

    simple.query()

What I tried

Because self.question is a QMessageBox.StandardButton, i.e. an enum, it has no method to allow clicking on it. I tried to set directly self.question to QMessageBox.Yes, for instance, but obviously, this does not close the window.

Also, adding self.question to qtbot with addWidget does not work, as it is no real widget.

Could this be due to a wrong design choice on my side? Should the opening of the question be in a separate function, and the rest of the logic would simply take as an input the result of the question. Or am I testing this the wrong way?

Additional but (maybe) related issues

It is seems related to me, but when calling an exec_ method on a customized QtGui.QDialog object, the same problem appears. I can not use the method qtbot.mouseClick on the object before clicking on it manually. Because the object pops out, and interrups the flow of the program.

Of course, this is the whole point of such an object in the main application, but while testing, it looks like it messes things up. Feel free to ignore this part if you deem it too unrelated (I can also open another ticket).

@nicoddemus
Copy link
Member

Hi Benjamin,

Well, this is an issue I find often at work with our xUnit code base. For QMessageBox.question specifically, I usually mock it:

def test_Qt(qtbot, mock):
    simple = Simple()
    qtbot.addWidget(simple)

    mock.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
    simple.query()
    assert simple.answer

(here I'm using pytest-mock, but using mock directly is fine too)

But the problem remains when you want to test a customized QDialog subclass. As you noted, exec_ enters its own loop and blocks main loop execution at that point.

One solution is to use a QTimer to fire an event some time after calling exec_():

def test_dialog(qtbot):
    dialog = Dialog()
    qtbot.addWidget(dialog)

    def interact():
        qtbot.click(dialog.button)

    QTimer.singleShot(500, interact)
    simple.query() # blocks

Another approach is to not call exec_ at all, using show() instead, which is fine if you are testing the dialog itself.

If you have some kind of Form widget that makes use of a custom QDialog in some of its interactions, I usually test the dialog separately using show(), and mock it when testing the Form itself like in the first example.

Cheers,

@baudren
Copy link
Author

baudren commented Sep 16, 2014

Hi,

thanks for the mock example for the question, it works flawlessly.

I tried both of your suggestion for the a Form widget running a QDialog, and only managed to make the QTimer solution work. It is probably due to my poor understanding of how the mock.patch.object work, but when I try to give a custom class as the first argument, and an attribute name as the second argument, it searches the attribute inside the class. Would you have a ressource other than this that explains how this works?

In any case, I am fine with QTimer. I would suggest that it appears somewhere in the documentation - maybe it is because I am a beginner tester, but I find extremely tough to find any information on how to properly test a Qt application. Since qtbot provides so much possibility, it would be great to expand its documentation. I would say that your answer could go directly after this part.

Thanks again for your time and detailled answers.

@nicoddemus
Copy link
Member

Thanks for the suggestion, I will add an example to the documentation (#19). 😄

About the example, I should have provided some code to make it clearer:

Suppose you have a custom dialog that asks the user for their name and age, and a Form that uses it. I usually add a convenience function that also has the nice benefit of making testing easier, like this:

class AskNameAndAgeDialog(QDialog):
    ...
    @classmethod
    def ask(cls, text, parent):
        dialog = cls(parent)
        dialog.text.setText(text)
        if dialog.exec_() == QDialog.Accepted:
            return dialog.getName(), dialog.getAge()
        else:
            return None, None                              

This allows clients of the dialog to use it as such:

name, age = AskNameAndAgeDialog.ask("Enter name and age because of bananas:", parent)
if name is not None:
    # use name and age for bananas

Now it is easy to mock AskNameAndAgeDialog.ask when testing Form:

def test_form_registration(qtbot, mock):
    form = RegistrationForm()

    mock.patch.object(AskNameAndAgeDialog, 'ask', return_value=('Jonh', 30))
    qtbot.click(form.enter_info()) # calls AskNameAndAgeDialog.ask
    # test that the rest of the form correctly behaves as if
    # user entered "Jonh" and 30 as name and age 

Hope that's clearer.

If you have any other questions, please don't hesitate to ask them. :)

Cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants