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:
qtbot (QtBot) –
runner (QtAsyncRunner) –
- Return type:
- 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
duringstart_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