Testing Asynchronous Qt Code (part 2)

Posted on Fri 03 July 2015 in programming by Ryan Day • Tagged with python, qt, testing

In part 1 of this post, we talked about Qt Connection Types and how it is important to understand these connection types when dealing with multiple threads. We're still using the same repo on Github with examples of a multi threaded GUI written with PySide (a Python Qt library).

But in this post we will try to uncover race conditions.

To get started, this tricky situation lies in this unit test. Using what we know about signal connection types, we know that there will be two events that must be processed after our button clicks.

def testRestoreList(self):
    self.main_window.name_text.setText('Ryan')
    self.main_window.submit_button.click()

    self.main_window.name_text.setText('Meg')
    self.main_window.submit_button.click()

    assert self.main_window.names_list.count() == 2

    self.main_window.clear_list_button.click()
    assert self.main_window.names_list.count() == 0

    self.main_window.restore_list_button.click()

    self.app.processEvents()        # Process the store_name queued event
    self.app.processEvents()        # Process the get_all_names queued event

    assert self.main_window.names_list.count() == 2

This test will always run correctly. But there is still a race condition! Let's go into the names_manager.store_name and add a QThread.sleep(1). This will simulate some delay between the signal and the actual storage of the name in whatever data store we would normally be using.

The test now fails. It fails because the Qthread.sleep(1) prevents us from writing the names to our data store before our test calls the get_all_names signal (after the restore_list_button click). This means we have to rely on our wait_for function to make sure our test is reliable.

def testRestoreList(self):
    self.main_window.name_text.setText('Ryan')
    self.main_window.submit_button.click()

    self.main_window.name_text.setText('Meg')
    self.main_window.submit_button.click()

    assert self.main_window.names_list.count() == 2
    assert wait_for(lambda: len(self.main_window.name_manager.names) == 2, 2) is True

    self.main_window.clear_list_button.click()
    assert self.main_window.names_list.count() == 0

    self.main_window.restore_list_button.click()

    self.app.processEvents()        # Process the store_name queued event
    self.app.processEvents()        # Process the get_all_names queued event

    assert self.main_window.names_list.count() == 2

We didn't add the wait_for to the bottom of the test though! That isn't the important part. What we need to do is be sure we wrote to our data store before we try to restore our names_list. That is why we add the wait_for function where we do.

QThread.sleep

Adding QThread.sleep() to your slots is a good way to verify that your tests are actually testing real world conditions in your application. Don't leave the QThread.sleep() in your slot of course! But when writing a new test, adding some fake delay to the slot will make sure that you aren't missing a race condition due the the nature of the test.