Testing#

qt-async-threads provides some fixtures and guidelines on how to test applications. The fixtures require pytest-qt.

Patterns#

When changing an existing slot into an async function, the test is affected because operations now not necessarily finish as soon as the slot is called.

For example, we might have the following test for the example in the Tutorial:

def test_download(qtbot: QtBot, tmp_path: Path) -> None:
    window = Window(tmp_path)
    qtbot.addWidget(window)

    window.count_spin.setValue(2)
    window.download_button.click()

    assert len(list(tmp_path.iterdir())) == 2

When we change the function to async, the test will likely fail, because as soon as the function hits the runner.run call, the download_button.click() will return, and the assertion will fail because the files will not have been downloaded yet.

We have some approaches:

Wait on the side-effect#

We can leverage QtBot.waitUntil to wait until a condition is met, here the condition being that we have 2 files downloaded in the directory:

def test_download(qtbot: QtBot, tmp_path: Path, runner: QtAsyncRunner) -> None:
    window = Window(tmp_path, runner)
    qtbot.addWidget(window)

    window.count_spin.setValue(2)
    window.download_button.click()

    def files_downloaded() -> None:
        assert len(list(tmp_path.iterdir())) == 2

    qtbot.waitUntil(files_downloaded)

QtBot.waitUntil will call files_downloaded() in a loop, until the condition does not raise an AssertionError or a time-out occurs.

Wait for the runner to become idle#

We can also use QtBot.waitUntil to wait for the runner to become idle after clicking on the button:

def test_download(qtbot: QtBot, tmp_path: Path, runner: QtAsyncRunner) -> None:
    window = Window(tmp_path, runner)
    qtbot.addWidget(window)

    window.count_spin.setValue(2)
    window.download_button.click()
    qtbot.waitUntil(runner.is_idle)

    assert len(list(tmp_path.iterdir())) == 2

Note

This approach only works if the signal is connected to the slot using a Qt.DirectConnection (which is the default).

If for some reason the connection is of the type Qt.QueuedConnection, this will not work because the signal will not be emitted directly by .click(), instead it will be scheduled for later delivery in the next pass of the event loop, and runner.is_idle() will be True.

Calling async functions#

If you need to call an async function directly in the test, you can use AsyncTester.start_and_wait to call it.

So it is possible to change from calling .click() directly to call the slot instead:

def test_download(
    qtbot: QtBot, tmp_path: Path, runner: QtAsyncRunner, async_tester: AsyncTester
) -> None:
    window = Window(tmp_path, runner)
    qtbot.addWidget(window)

    window.count_spin.setValue(2)
    async_tester.start_and_wait(window._on_download_button_clicked())

    assert len(list(tmp_path.iterdir())) == 2

Here we change from calling .click() to call the slot directly just to exemplify, it is recommended to call functions which emit the signal when possible as they ensure the signal and slot are connected.

However the technique is useful to test async functions in isolation when using this library.

Fixtures/classes reference#

qt_async_threads.pytest_plugin.runner(qtbot)#

Returns a QtAsyncRunner, shutting it down at the end of the test.

Parameters:

qtbot (QtBot) –

Return type:

Iterator[QtAsyncRunner]

qt_async_threads.pytest_plugin.async_tester(qtbot, runner)#

Return an AsyncTester, with utilities to handling async calls in tests.

Parameters:
Return type:

AsyncTester

class qt_async_threads.pytest_plugin.AsyncTester(runner, qtbot, timeout_s=5)#

Testing helper for async functions.

Parameters:
  • runner (QtAsyncRunner) –

  • qtbot (QtBot) –

  • timeout_s (int) –

timeout_s: int#

Timeout in seconds for QtBot.waitUntil during start_and_wait.

start_and_wait(coroutine, *, timeout_s=None)#

Starts the given coroutine and wait for the runner to be idle.

Note this is not exactly the same as calling run_coroutine, because the former waits only for the given coroutine, while this method waits for the runner itself to become idle (meaning this will wait even if the given coroutine starts other coroutines).

Parameters:
  • timeout_s (int | None) – If given, how long to wait. If not given, will use AsyncTester.timeout_s.

  • coroutine (Coroutine) –

Return type:

None