How to Test a Qt Application (No Object Names Required)
Qt testing for real installed apps, with plain-language steps instead of object names or Squish scripts.
Automate and scale manual testing with AI ->
Above: the test below running against qBittorrent, sped up with idle time removed.
You can do Qt testing end-to-end without object names by describing each step in plain language. TestDriver matches each description against screenshots of the installed app, so:
- you test the same package your users install, with no test hooks built in
- you don’t set or maintain
objectNamevalues - your test reads like the steps you’d give a teammate
TestDriver generates and runs black-box end-to-end tests. It starts your app in a fresh virtual machine, looks at the screen and uses a real mouse and keyboard, so it never needs selectors or access to your code. Each test is a normal Vitest file, and every run is recorded as a replay.
The example installs qBittorrent, an open-source Qt Widgets app, turns on its Web UI on a new port, and checks the setting is saved. The full project is on GitHub: testdriverai/example-qt-app.
Why not Squish or Qt Test
Qt Test simulates mouse and keyboard events on widgets from inside a test binary you compile. Squish, Qt’s commercial GUI testing tool, drives the running app and finds widgets by their properties, such as type, text and objectName:
# Squish-style object lookup (illustrative, not run)
clickButton(waitForObject({"type": "QPushButton", "text": "OK", "window": names.preferences_QDialog}))
TestDriver describes the same button the way a user sees it:
await testdriver.find("the OK button with a green checkmark at the bottom right of the Preferences dialog").click();
The property lookup breaks when a widget is renamed or restructured. The description only changes when the UI does.
Watch the full replay
The replay shows every step with its screenshot and log.
The test
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("qBittorrent (Qt)", () => {
it("saves a new Web UI port in Preferences", async (context) => {
const testdriver = TestDriver(context);
// Install qBittorrent from the Ubuntu archive.
await testdriver.exec(
"sh",
"sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq qbittorrent",
300000,
);
// Start the replay recording, then launch the app.
await testdriver.provision.dashcam({ title: "qBittorrent (Qt) Web UI port", webLogs: false });
await testdriver.exec("sh", "nohup qbittorrent >/tmp/qbittorrent.log 2>&1 &", 10000);
// The first launch shows a legal notice. Wait for it and accept.
await testdriver.find("the I Agree button in the qBittorrent Legal Notice dialog", { timeout: 60000 }).click();
// Open Tools > Preferences and go to the Web UI page.
await testdriver.find("the Tools menu in the qBittorrent menu bar").click();
await testdriver.find("the Preferences item in the open Tools menu").click();
await testdriver.find("the Web UI section in the left sidebar of the Preferences dialog").click();
// Turn on the Web UI and change its port from 8080 to 9090.
await testdriver.find("the Web User Interface (Remote control) checkbox at the top of the Web UI page").click();
await testdriver.find("the Port field showing 8080 next to the IP address field on the Web UI page").click();
await testdriver.pressKeys(["ctrl", "a"]);
await testdriver.type("9090");
// Save and close.
await testdriver.find("the OK button with a green checkmark at the bottom right of the Preferences dialog").click();
// Reopen Preferences on the Web UI page.
await testdriver.find("the Tools menu in the qBittorrent menu bar").click();
await testdriver.find("the Preferences item in the open Tools menu").click();
await testdriver.find("the Web UI section in the left sidebar of the Preferences dialog").click();
// Wait for the saved port, then check both settings stuck.
await testdriver.find("the Port field showing 9090 on the Web UI page", { timeout: 15000 });
const saved = await testdriver.assert(
"the qBittorrent Preferences dialog shows the Web UI page with the Web User Interface (Remote control) checkbox checked and the Port field set to 9090",
);
expect(saved).toBeTruthy();
});
});
How it works
Install and launch the app
The first exec() installs qBittorrent from the Ubuntu archive on a fresh Linux sandbox. provision.dashcam() starts the replay recording, and the second exec() launches the app in the background.
Describe elements by what users see
Each find() takes a description that names the label, the look and the position, such as “the Port field showing 8080 next to the IP address field”. The test never reads Qt’s object tree, so you don’t need objectName values or accessibility names. See locating elements.
Wait for results, then assert
find() with a timeout keeps looking until the element appears, so the test waits for the first window and the saved port without a fixed sleep. assert() then checks a plain-language statement against the screen and returns true or false. See making assertions.
Run it yourself
Clone the example repo, or set up TestDriver in your own project and run the test:
npx testdriverai init
npx vitest run qt-app.test.mjs
To test your own Qt app:
- Replace the
apt-get installline with your own package, or useprovision.installer()with your release.deb. - Start your binary in the second
exec()call. - On Windows, pass
os: "windows"toTestDriver()and install your.msior.exewithprovision.installer().
FAQ
Do I need to set objectName on my widgets? No. Steps are matched on screenshots, so there are no object names or test IDs to add or keep in sync.
Does it work with QML apps? TestDriver only sees the rendered window, so it doesn’t depend on whether the UI is Qt Widgets or Qt Quick. This demo uses a Widgets app.
Can I run it in CI? Yes. It’s a normal Vitest file and the app runs in TestDriver’s cloud sandbox. See CI/CD.
Automate and scale manual testing with AI
TestDriver uses computer-use AI to test any app - write tests in plain English and run them anywhere.