Глава 45 – Проведение тестов

Эта глава раскроет неожиданный поворот мышления: как перестать бояться изменений в коде и начать вносить их уверенно. Вы обнаружите, почему профессиональные разработчики тратят меньше времени на отладку, узнаете секрет быстрого выявления регрессий и раскроем, как тесты превращаются в ваш защитный механизм при каждом изменении.

Без лишних спойлеров: речь пойдет о модульных тестах, модуле QtTest, проверках через QCOMPARE и QVERIFY, а также о тестах с передачей данных, которые позволяют сократить дублирование кода в разы и повысить надежность критических участков.

Пропуск этой главы — риск вернуться к многочасовой отладке. Прочтение — шанс изменить стиль разработки навсегда.

Для закрепления материала доступны готовые к компиляции примеры и 16 бесплатных глав, позволяющих сразу применять подходы из книги на практике.

Самопроверка по главе

В чём ключевое отличие между модульными (unit tests) и системными тестами (system tests)?Ответ
Правильный ответ: Модульные тесты проверяют каждый класс изолированно, предполагая что всё остальное работает корректно. Системные тесты проверяют всё приложение целиком с взаимодействием GUI и всех компонентов.
Почему рекомендуется запускать тесты после каждой компиляции, а не периодически?Ответ
Правильный ответ: Частый запуск тестов позволяет сразу локализовать ошибку, поскольку вы точно знаете что изменилось с последней компиляции. Это резко сокращает время отладки и предотвращает накопление ошибок.
Зачем создавать тесты до написания реализации кода класса?Ответ
Правильный ответ: Написание теста заставляет лучше осмыслить задачу и задать вопрос «что нужно сделать для добавления реализации». Это улучшает проектирование и выявляет проблемы на раннем этапе.
Почему не стоит стремиться написать тесты для всех классов и всех возможных случаев?Ответ
Правильный ответ: Чрезмерное усердие может создать ощущение что тестирование отнимает слишком много времени, и вы откажетесь от них совсем. Лучше писать тесты для подозрительных критических мест и граничных условий.
Зачем при первом запуске теста намеренно модифицировать код так, чтобы тест провалился?Ответ
Правильный ответ: Это проверяет что тест действительно выполняется и способен обнаруживать ошибки. Если тест проходит даже с заведомо неправильным кодом, значит тест не работает как задумано.
Что произойдёт, если в макросе QFETCH() указать имя переменной, которое не совпадает с именем столбца в таблице данных?Ответ
Правильный ответ: Тест завершится с сообщением об ошибке, так как элемент данных с указанным именем не будет найден в таблице тестовых данных.
В чём преимущество тестов с передачей данных (data-driven tests) по сравнению с вписыванием данных прямо в QCOMPARE()?Ответ
Правильный ответ: Отделение тестового кода от данных минимизирует дублирование. Новые тестовые случаи можно легко добавлять в слот _data() без модификации самого теста.
Почему тестовый класс должен наследоваться от QObject и содержать макрос Q_OBJECT?Ответ
Правильный ответ: Это необходимо для создания метаинформации, которая позволяет вызывать слоты класса при исполнении, включая тестовые слоты через механизм интроспекции Qt.
В каком случае следует использовать QVERIFY() вместо QCOMPARE()?Ответ
Правильный ответ: QVERIFY() используется когда нужно проверить истинность условия или булево выражение (например, isModified()), а QCOMPARE() — для сравнения двух конкретных значений.
Какой метод QTest нужно использовать для симуляции ввода строки текста в виджет, и почему не стоит симулировать каждое нажатие клавиши отдельно?Ответ
Правильный ответ: Используйте QTest::keyClicks() для ввода строки целиком. Это проще и быстрее чем множественные вызовы keyClick() для каждого символа, хотя последний даёт больше контроля.
Что произойдёт с выполнением теста, если исправление одной ошибки создаст новую ошибку в другом месте кода?Ответ
Правильный ответ: При частом запуске тестов новая ошибка будет обнаружена сразу после компиляции, поскольку тесты проверяют весь функционал. Без тестов такая регрессия может остаться незамеченной надолго.
Для чего служат методы initTestCase() и cleanupTestCase(), и когда они вызываются?Ответ
Правильный ответ: Они вызываются в начале и конце выполнения всех тестов соответственно (не каждого теста) и служат для инициализации общих ресурсов и финальной очистки.
Как использовать параметр -eventdelay для поиска ошибок в GUI, и в чём его польза?Ответ
Правильный ответ: Параметр заставляет тест делать паузу между событиями, позволяя визуально наблюдать что происходит в интерфейсе. Это помогает найти ошибки связанные с асинхронными обновлениями и перерисовкой виджетов.

Практические задания

Простой уровень

Тест математических операций с граничными значениями
Создайте класс Calculator с методами add(), subtract(), multiply() и divide(). Напишите модульный тест Test_Calculator с проверкой граничных условий: деление на ноль, работа с отрицательными числами, переполнение при больших числах, операции с нулём. Используйте подход с передачей данных (data-driven testing) для каждого метода.
Подсказки: Создайте слоты add_data(), subtract_data() и т.д. В таблице данных предусмотрите строки для граничных случаев: (0, 0), (INT_MAX, 1), (-5, -3), (10, 0) для divide(). Для деления на ноль можно проверить что результат равен специальному значению или бросается исключение. Не забудьте про макрос Q_OBJECT и включение test.moc.

Средний уровень

Тестирование пользовательского виджета с событиями
Создайте виджет CustomCounter на основе QPushButton с внутренним счётчиком, который увеличивается при каждом клике и меняет текст кнопки на «Clicked: N раз». Напишите тест который симулирует 5 кликов мышью, проверяет корректность текста кнопки после каждого клика, использует QTestEventList для записи последовательности событий, и проверяет что сигнал clicked() испускается правильное количество раз с помощью QSignalSpy.
Подсказки: В тестовом классе используйте QTest::mouseClick() для симуляции. QSignalSpy spy(&button, &QPushButton::clicked) поможет подсчитать сигналы — проверьте spy.count(). QTestEventList позволяет записать серию действий через addMouseClick() и воспроизвести через simulate(). Добавьте небольшие задержки между кликами через addDelay() для более реалистичного теста.

Сложный уровень

Комплексное тестирование формы ввода данных
Создайте форму LoginForm с QLineEdit для логина и пароля, QCheckBox «Запомнить меня» и QPushButton «Войти». Кнопка должна быть неактивна пока оба поля не заполнены. Напишите полноценный тестовый модуль который: 1) проверяет начальное состояние формы, 2) симулирует ввод данных через клавиатуру, 3) проверяет активацию кнопки, 4) тестирует валидацию (логин минимум 3 символа, пароль 6 символов), 5) симулирует клик на кнопку и проверяет испускание сигнала loginAttempted(QString, QString, bool), 6) использует параметры запуска для вывода XML отчёта. Создайте также CMakeLists.txt для сборки теста.
Подсказки: Разделите тестирование на методы: testInitialState(), testDataInput(), testValidation(), testLoginAttempt(). Используйте QSignalSpy для проверки сигналов. Для проверки активности кнопки используйте QVERIFY(button.isEnabled()). В CMakeLists.txt подключите Qt6::Test и Qt6::Widgets, используйте qt_add_test(). Запустите тест с параметром -o results.xml,xml для получения XML отчёта. Не забудьте про методы initTestCase() для создания формы и cleanupTestCase() для очистки.

💬 Присоединяйтесь к обсуждению!

Освоили модульное тестирование в Qt? Возникли вопросы о том, как правильно организовать тесты для сложных классов или как симулировать сложные пользовательские сценарии?

Делитесь своим опытом автоматизации тестирования, обсуждайте сложные кейсы с data-driven подходом и помогайте другим читателям внедрить культуру тестирования в свои Qt-проекты!

Leave a Reply

Your email address will not be published. Required fields are marked *