This chapter reveals an unexpected mindset shift: how to stop fearing code changes and start making them confidently. You’ll discover why professional developers spend less time debugging, learn the secret of quickly identifying regressions, and uncover how tests become your defense mechanism with every change.
Without spoilers: we’ll discuss unit tests, the QtTest module, checks via QCOMPARE and QVERIFY, and data-driven tests that allow reducing code duplication multiple times and increasing reliability of critical sections.
Skipping this chapter is a risk of returning to hours of debugging. Reading it is a chance to change your development style forever.
This chapter includes ready-to-use code examples.
Chapter Self-Check
What’s the key difference between unit tests and system tests?Answer
Correct answer: Unit tests check each class in isolation, assuming everything else works correctly. System tests check the entire application as a whole with GUI interaction and all components.
Why is it recommended to run tests after every compilation, rather than periodically?Answer
Correct answer: Frequent test runs allow you to immediately localize errors, since you know exactly what changed since the last compilation. This drastically reduces debugging time and prevents error accumulation.
Why create tests before writing class implementation code?Answer
Correct answer: Writing a test forces you to better understand the task and ask “what needs to be done to add implementation.” This improves design and reveals problems early.
Why shouldn’t you strive to write tests for all classes and all possible cases?Answer
Correct answer: Excessive diligence can create the feeling that testing takes too much time, and you’ll abandon it altogether. It’s better to write tests for suspicious critical spots and boundary conditions.
Why intentionally modify code to fail the test on first run?Answer
Correct answer: This verifies that the test actually executes and can detect errors. If the test passes even with deliberately wrong code, it means the test doesn’t work as intended.
What happens if in the QFETCH() macro you specify a variable name that doesn’t match a column name in the data table?Answer
Correct answer: The test will terminate with an error message, as the data element with the specified name won’t be found in the test data table.
What’s the advantage of data-driven tests compared to putting data directly in QCOMPARE()?Answer
Correct answer: Separating test code from data minimizes duplication. New test cases can be easily added in the _data() slot without modifying the test itself.
Why must the test class inherit from QObject and contain the Q_OBJECT macro?Answer
Correct answer: This is necessary to create meta-information that allows calling class slots during execution, including test slots through Qt’s introspection mechanism.
In which case should you use QVERIFY() instead of QCOMPARE()?Answer
Correct answer: QVERIFY() is used when you need to check the truth of a condition or boolean expression (e.g., isModified()), while QCOMPARE() is for comparing two specific values.
Which QTest method should be used to simulate entering a text string into a widget, and why shouldn’t you simulate each key press separately?Answer
Correct answer: Use QTest::keyClicks() to enter the entire string. This is simpler and faster than multiple keyClick() calls for each character, though the latter gives more control.
What will happen to test execution if fixing one bug creates a new bug elsewhere in the code?Answer
Correct answer: With frequent test runs, the new bug will be detected immediately after compilation, since tests check all functionality. Without tests, such regression can remain unnoticed for a long time.
What are the initTestCase() and cleanupTestCase() methods for, and when are they called?Answer
Correct answer: They’re called at the start and end of executing all tests respectively (not each test) and serve for initializing common resources and final cleanup.
How can you use the -eventdelay parameter to find GUI bugs, and what’s its benefit?Answer
Correct answer: The parameter forces the test to pause between events, allowing you to visually observe what’s happening in the interface. This helps find bugs related to asynchronous updates and widget redraws.
Practical Assignments
Easy Level
Testing Math Operations with Boundary Values
Create a Calculator class with add(), subtract(), multiply(), and divide() methods. Write a Test_Calculator unit test checking boundary conditions: division by zero, working with negative numbers, overflow with large numbers, operations with zero. Use data-driven testing approach for each method.
Hints: Create add_data(), subtract_data() slots, etc. In the data table, include rows for boundary cases: (0, 0), (INT_MAX, 1), (-5, -3), (10, 0) for divide(). For division by zero, check that result equals a special value or an exception is thrown. Don’t forget the Q_OBJECT macro and including test.moc.
Medium Level
Testing Custom Widget with Events
Create a CustomCounter widget based on QPushButton with an internal counter that increases with each click and changes button text to “Clicked: N times”. Write a test that simulates 5 mouse clicks, checks button text correctness after each click, uses QTestEventList to record event sequences, and verifies that the clicked() signal is emitted the correct number of times using QSignalSpy.
Hints: In the test class, use QTest::mouseClick() for simulation. QSignalSpy spy(&button, &QPushButton::clicked) will help count signals — check spy.count(). QTestEventList allows recording action series via addMouseClick() and playing back via simulate(). Add small delays between clicks via addDelay() for a more realistic test.
Hard Level
Comprehensive Data Entry Form Testing
Create a LoginForm with QLineEdit for login and password, QCheckBox “Remember me”, and QPushButton “Login”. The button should be inactive until both fields are filled. Write a full test module that: 1) checks initial form state, 2) simulates keyboard data input, 3) checks button activation, 4) tests validation (login minimum 3 chars, password 6 chars), 5) simulates button click and checks loginAttempted(QString, QString, bool) signal emission, 6) uses launch parameters to output XML report. Also create CMakeLists.txt for building the test.
Hints: Split testing into methods: testInitialState(), testDataInput(), testValidation(), testLoginAttempt(). Use QSignalSpy for signal checking. For button activity check, use QVERIFY(button.isEnabled()). In CMakeLists.txt, link Qt6::Test and Qt6::Widgets, use qt_add_test(). Run test with -o results.xml,xml parameter to get XML report. Don’t forget initTestCase() methods for creating the form and cleanupTestCase() for cleanup.
💬 Join the Discussion!
Mastered unit testing in Qt? Have questions about how to properly organize tests for complex classes or how to simulate complex user scenarios?
Share your test automation experience, discuss complex cases with data-driven approach, and help other readers implement testing culture in their Qt projects!