Spec-Zone.ru › Qt 6.0

Лучшие практики тестирования Qt

Рекомендуется добавлять тесты Qt для исправления ошибок и новых функций. Перед попыткой исправить ошибку, добавьте регрессионный тест (желательно автоматический), который завершается неудачей до исправления, демонстрируя ошибку, и проходит после исправления. Во время разработки новых функций добавляйте тесты для проверки того, что они работают так, как задумано.

Следование набору стандартов кодирования повысит вероятность надежной работы автоматических тестов Qt во всех средах. Например, некоторые тесты должны считывать данные с диска. Если нет установленных стандартов для того, как это делается, некоторые тесты не будут переносимыми. Например, тест, который предполагает, что файлы тестовых данных находятся в текущем рабочем каталоге, работает только для сборки из исходного кода. При сборке в отдельной директории (вне директории исходного кода) тест не сможет найти свои данные.

В следующих разделах приведены рекомендации по написанию тестов Qt:

  • Общие принципы
  • Создание надежных тестов
  • Улучшение выходных данных тестов
  • Написание тестируемого кода
  • Настройка тестовых машин

Общие принципы

В следующих разделах приведены общие рекомендации по написанию модульных тестов:

  • Проверка тестов
  • Давать тестовым функциям описательные имена
  • Написание автономных тестовых функций
  • Тестирование всего стека
  • Быстрое выполнение тестов
  • Использование данных для тестирования
  • Использование инструментов покрытия кода
  • Выбор соответствующих механизмов для исключения тестов
  • Избегайте использования Q_ASSERT

Проверка тестов

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

Например, рабочий процесс исправления ошибки в классе QDateTime может быть таким, если используется система управления версиями Git:

  1. Создайте ветку для исправления и тестирования: git checkout -b fix-branch 5.14
  2. Напишите тест и исправьте ошибку.
  3. Соберите и протестируйте как исправление, так и новый тест, чтобы убедиться, что новый тест проходит с исправление.
  4. Добавьте исправление и тест в вашу ветку: git add tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp src/corelib/time/qdatetime.cpp
  5. Зафиксируйте исправление и тест в вашей ветке: git commit -m 'Fix bug in QDateTime'
  6. Для проверки того, что тест действительно ловит то, для чего вам нужно было исправление, переключитесь на ветку, на которой вы основывали свою ветку: git checkout 5.14
  7. Переключитесь только на файл теста на ветку 5.14: git checkout fix-branch -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp

    Теперь только тест находится на ветке исправления. Остальная часть дерева исходного кода по-прежнему находится на 5.14.

  8. Соберите и запустите тест, чтобы убедиться, что он завершается неудачей на 5.14, и, следовательно, действительно ловит ошибку.
  9. Теперь вы можете вернуться на ветку исправления: git checkout fix-branch
  10. В качестве альтернативы, можно восстановить рабочую область до чистого состояния на 5.14: git checkout HEAD -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp

При просмотре изменений вы можете адаптировать этот рабочий процесс, чтобы проверить, сопровождается ли изменение тестом для проблемы, которую оно исправляет.

Давать тестовым функциям описательные имена

Именование тестовых случаев важно. Имя теста отображается в отчете об ошибке при выполнении теста. Для тестов с данными также отображается имя строки данных в отчете об ошибке. Имена предоставляют тем, кто читает отчет, первое представление о том, что пошло не так.

Имена тестовых функций должны четко указывать, что тестирует функция. Не используйте просто идентификатор отслеживания ошибок, поскольку идентификаторы устаревают при замене системы отслеживания ошибок. Кроме того, некоторые системы отслеживания ошибок могут быть недоступны для всех пользователей. Если отчет об ошибке может представлять интерес для будущих читателей кода теста, вы можете упомянуть его в комментарии рядом с соответствующей частью теста.

Аналогичным образом, при написании тестов с данными присваивайте описательные имена тестовым случаям, которые указывают на какой аспект функциональности ориентирован каждый случай. Не просто нумеруйте тестовые случаи или используйте идентификаторы отслеживания ошибок. Человек, читающий вывод теста, не будет знать, что означают числа или идентификаторы. Можно добавить комментарий к строке теста, в котором упоминается идентификатор отслеживания ошибки, если это уместно.

Написание автономных тестовых функций

Внутри тестовой программы тестовые функции должны быть независимы друг от друга, и они не должны полагаться на выполнение предыдущих тестовых функций. Вы можете проверить это, запустив тестовую функцию самостоятельно с tst_foo testname.

Не используйте экземпляры класса, подлежащего тестированию, в нескольких тестах. Экземпляры тестов (например, виджеты) не должны быть переменными-членами тестов, а предпочтительно создаваться на стеке для обеспечения надлежащей очистки даже при неудаче теста, чтобы тесты не мешали друг другу.

Тестирование всего стека

Если API реализован в терминах подключаемых или платформенно-специфических бэкендов, которые выполняют тяжелую работу, убедитесь, что написаны тесты, охватывающие все пути кода до бэкендов. Тестирование верхних уровней API с помощью эмулятора бэкенда является хорошим способом изолировать ошибки в слое API от бэкендов, но это дополняет тесты, которые выполняют фактическую реализацию с реальными данными.

Быстрое выполнение тестов

Тесты не должны тратить время на излишнюю повторяемость, использование необоснованно больших объемов тестовых данных или введение ненужных простоев.

Это особенно актуально для модульного тестирования, где каждая дополнительная секунда времени выполнения модульного теста увеличивает время тестирования ветки в CI на нескольких целевых платформах. Помните, что модульное тестирование отличается от нагрузочного и тестирования надежности, где ожидаются большие объемы тестовых данных и более длительные тестовые заезды.

Тесты производительности, которые обычно выполняют один и тот же тест несколько раз, должны располагаться в отдельной директории tests/benchmarks и не должны смешиваться с функциональными модульными тестами.

Использование данных для тестирования

Тесты с данными упрощают добавление новых тестов для граничных условий, найденных в последующих отчетах об ошибках.

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

Использование инструментов покрытия кода

Используйте инструмент покрытия кода, такой как Froglogic Coco Code Coverage или gcov, чтобы помочь написать тесты, которые покрывают как можно больше утверждений, ветвей и условий в тестируемой функции или классе. Чем раньше это будет сделано на этапе разработки новой функции, тем проще будет поймать регрессии позже, когда код будет переработан.

Выбор соответствующих механизмов для исключения тестов

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

Используйте QSKIP(), чтобы обработать случаи, когда вся тестовая функция оказывается неприменимой во время выполнения в текущей среде тестирования. При необходимости пропустить только часть тестовой функции, можно использовать условный оператор, необязательно с вызовом qDebug() для сообщения о причине пропуска неприменимой части.

Тестовые функции или строки данных тестового случая с данными могут быть ограничены конкретными платформами или конкретными включенными функциями с помощью #if. Однако будьте осторожны с ограничениями moc при использовании #if для пропуска тестовых функций. Предпроцессор moc не имеет доступа ко всем макросам builtin компилятора, которые часто используются для обнаружения функций компилятора. Поэтому moc может получить другой результат для условия предпроцессора, чем остальная часть вашего кода. Это может привести к тому, что moc сгенерирует метаданные для тестового слота, который фактически пропустит компилятор, или опустит метаданные для тестового слота, который фактически будет скомпилирован в класс. В первом случае тест попытается запустить слот, который не реализован. Во втором случае тест не попытается запустить тестовый слот, даже если он должен.

Если вся тестовая программа неприменима для определенной платформы или если не включена определенная функция, лучшим подходом является использование файла .pro родительской директории для предотвращения сборки теста. Например, если тест tests/auto/gui/someclass недействителен для macOS, добавьте следующую строку в tests/auto/gui.pro:

mac*: SUBDIRS -= someclass

Избегайте использования Q_ASSERT

Макрос Q_ASSERT приводит к прерыванию программы всякий раз, когда проверяемое условие false, но только если программное обеспечение было собрано в отладочном режиме. В сборках и релизных и отладочных сборках Q_ASSERT ничего не делает.

Q_ASSERT следует избегать, потому что это делает поведение тестов различным в зависимости от того, тестируется ли отладочная сборка, и потому что это заставляет тест прерваться сразу, пропуская все оставшиеся тестовые функции и возвращая неполные или неправильно сформированные результаты тестов.

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

Вместо Q_ASSERT, следует использовать варианты макросов QCOMPARE() или QVERIFY(). Они заставляют текущий тест сообщить об ошибке и завершиться, но позволяют выполнить оставшиеся тестовые функции и завершить всю тестовую программу нормально. QVERIFY2() даже позволяет записывать описательное сообщение об ошибке в журнал тестов.

Создание надежных тестов

В следующих разделах приведены рекомендации по созданию надежных тестов:

  • Избегайте побочных эффектов в этапах проверки
  • Избегайте фиксированных таймаутов
  • Будьте осторожны с зависящим от времени поведением
  • Избегайте захвата и сравнения растровых изображений

Избегайте побочных эффектов в этапах проверки

При выполнении шагов проверки в автотесте с использованием QCOMPARE(), QVERIFY() и т. д., следует избегать побочных эффектов. Побочные эффекты в шагах проверки могут затруднить понимание теста. Кроме того, они могут легко нарушить тест способами, которые трудно диагностировать, когда тест изменяется на использование QTRY_VERIFY(), QTRY_COMPARE() или QBENCHMARK(). Эти функции могут выполнить переданное выражение несколько раз, тем самым повторяя любые побочные эффекты.

Когда побочные эффекты неизбежны, убедитесь, что исходное состояние восстанавливается в конце функции теста, даже если тест завершился ошибкой. Это обычно требует использования класса RAII (приобретение ресурса — инициализация), который восстанавливает состояние при возвращении из функции, или метода cleanup(). Не просто помещайте код восстановления в конец теста. Если часть теста завершается ошибкой, такой код будет пропущен, и исходное состояние не будет восстановлено.

Избегайте фиксированных таймаутов

Избегайте использования жестко заданных таймаутов, таких как QTest::qWait(), для ожидания, пока некоторые условия не станут истинными. Рассмотрите возможность использования класса QSignalSpy, макросов QTRY_VERIFY() или QTRY_COMPARE(), или класса QSignalSpy в сочетании с вариантами макроса QTRY_.

Функция qWait() может использоваться для задания задержки на фиксированный период времени между выполнением некоторого действия и ожиданием завершения асинхронного поведения, вызванного этим действием. Например, изменение состояния виджета и ожидание его перерисовки. Однако такие таймауты часто приводят к ошибкам, когда тест, написанный на рабочей станции, выполняется на устройстве, где ожидаемое поведение может занимать больше времени. Увеличение фиксированного таймаута до значения, в несколько раз превышающего необходимое значение на самой медленной платформе тестирования, не является хорошим решением, так как это замедляет выполнение теста на всех платформах, особенно для таблично-управляемых тестов.

Если код, который тестируется, выдает Qt-сигналы при завершении асинхронного поведения, лучшим подходом является использование класса QSignalSpy для уведомления функции теста о том, что шаг проверки теперь можно выполнить.

Если сигналов Qt нет, используйте макросы QTRY_COMPARE() и QTRY_VERIFY(), которые периодически проверяют указанное условие, пока оно не станет истинным или не будет достигнут максимальный таймаут. Эти макросы предотвращают задержку выполнения теста дольше, чем необходимо, одновременно избегая сбоев, когда тесты пишутся на рабочих станциях и затем выполняются на встроенных платформах.

Если нет сигналов Qt и вы пишете тест в рамках разработки нового API, подумайте, не мог бы API получить выгоду от добавления сигнала, сообщающего о завершении асинхронного поведения.

Будьте осторожны с зависящим от времени поведением

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

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

При тестировании классов, которые изменяют свое состояние на основе событий таймера, необходимо учитывать поведение, основанное на таймере, при выполнении шагов проверки. Из-за разнообразия зависящего от времени поведения универсального решения для этой проблемы тестирования нет.

Для виджетов ввода текста возможные решения включают отключение мигания курсора (если API предоставляет эту функцию), ожидание, пока курсор не окажется в известном состоянии перед захватом растрового изображения (например, путем подписки на соответствующий сигнал, если API предоставляет такой сигнал), или исключение области, содержащей курсор, из сравнения растровых изображений.

Избегайте захвата и сравнения растровых изображений

Хотя проверка результатов теста путем захвата и сравнения растровых изображений иногда необходима, она может быть довольно хрупкой и трудоемкой.

Например, определённый виджет может иметь различный вид на разных платформах или с разными стилями виджетов, поэтому эталонные растровые изображения могут потребоваться создавать несколько раз и затем поддерживать их в будущем по мере развития набора поддерживаемых платформ Qt. Изменения, влияющие на растровое изображение, означают необходимость пересоздания ожидаемых растровых изображений на каждой поддерживаемой платформе, что потребовало бы доступа к каждой платформе.

Сравнение растровых изображений также может зависеть от таких факторов, как разрешение экрана тестовой машины, глубина цвета, активная тема, цветовая схема, стиль виджета, активный язык (символы валют, направление текста и т. д.), размер шрифта, эффекты прозрачности и выбор менеджера окон.

В тех случаях, когда это возможно, используйте программно-управляемые средства, такие как проверка свойств объектов и переменных, вместо захвата и сравнения растровых изображений.

Улучшение выходных данных теста

В следующих разделах приведены рекомендации по созданию удобочитаемых и полезных выходных данных теста:

  • Явно игнорировать ожидаемые предупреждения
  • Избегайте вывода сообщений отладки из автотестов
  • Напишите хорошо структурированный диагностический код

Явно игнорировать ожидаемые предупреждения

Если ожидается, что тест приведет к выводу Qt предупреждения или сообщения отладки в консоль, следует вызвать QTest::ignoreMessage(), чтобы отфильтровать это сообщение из выходных данных теста и завершить тест ошибкой, если сообщение не выведено.

Если такое сообщение выводится только при создании Qt в режиме отладки, используйте QLibraryInfo::isDebugBuild(), чтобы определить, были ли библиотеки Qt скомпилированы в режиме отладки. Использование #ifdef QT_DEBUG недостаточно, так как оно только сообщит вам, был ли тест скомпилирован в режиме отладки, и это не гарантирует, что библиотеки Qt также были скомпилированы в режиме отладки.

Избегайте вывода сообщений отладки из автотестов

Автотесты не должны генерировать какие-либо необработанные предупреждения или сообщения отладки. Это позволит шлюзу непрерывной интеграции рассматривать новые предупреждения или сообщения отладки как ошибки теста.

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

Напишите хорошо структурированный диагностический код

Любые диагностические выходные данные, которые могут быть полезными при ошибке теста, должны быть частью стандартных выходных данных теста, а не быть закомментированными, отключенными директивами препроцессора или включенными только в дебаг-версии. Если тест завершается неудачей во время непрерывной интеграции, наличие всех соответствующих диагностических выходных данных в логах CI может сэкономить вам много времени по сравнению с включением диагностического кода и повторным тестированием. Особенно, если сбой произошёл на платформе, которой у вас нет на настольном компьютере.

Диагностические сообщения в тестах должны использовать механизмы вывода Qt, такие как qDebug() и qWarning(), а не stdio.h или iostream.h механизмы вывода. Последние обходят обработку сообщений Qt и препятствуют тому, чтобы команда -silent командной строки подавляла диагностические сообщения. В результате важные сообщения об ошибках могут скрыться в большом объеме отладочной информации.

Написание легко тестируемого кода

В следующих разделах приведены рекомендации по написанию кода, который легко тестируется:

  • Разрыв зависимостей
  • Компиляция всех классов в библиотеки

Разрыв зависимостей

Идея модульного тестирования заключается в использовании каждого класса изолированно. Поскольку многие классы инициализируют другие классы, изолированная инициализация одного класса невозможна. Поэтому необходимо использовать технику, называемую «инъекцией зависимостей», которая разделяет создание объектов от использования объектов. Фабрика отвечает за построение древовидных структур объектов. Другие объекты взаимодействуют с этими объектами через абстрактные интерфейсы.

Эта техника хорошо работает для приложений, ориентированных на данные. Для приложений с графическим интерфейсом такой подход может быть сложным, поскольку объекты часто создаются и уничтожаются. Для проверки правильного поведения классов, зависящих от абстрактных интерфейсов, может использоваться «мокинг». Например, см. Googletest Mocking (gMock) Framework.

Компиляция всех классов в библиотеки

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

Проще перечислить исходные файлы и заголовки только один раз в скрипте для создания статической библиотеки. Затем функция main() будет связана со статической библиотекой для создания исполняемого файла, а тесты будут связаны со статическими библиотеками.

Для проектов, в которых одни и те же исходные файлы используются для создания нескольких программ, может быть целесообразнее собрать общие классы в динамически подключаемую (или объектную) библиотеку, которую каждая программа, включая программы тестирования, может загружать во время выполнения. Опять же, наличие скомпилированного кода в библиотеке помогает избежать дублирования в описании компонентов, которые нужно объединить для создания различных программ.

Настройка тестовых машин

В следующих разделах рассматриваются распространённые проблемы, связанные с настройкой тестовых машин:

  • Заставки
  • Системные диалоги
  • Использование дисплея
  • Менеджеры окон

Все эти проблемы обычно могут быть решены с помощью виртуализации.

Заставки

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

Системные диалоги

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

Примеры типичных проблем включают диалоги уведомлений об обновлении в режиме онлайн на macOS, ложные срабатывания антивирусных сканеров, запланированные задачи, такие как обновление сигнатур вирусов, обновления программного обеспечения, рассылаемые на рабочие станции, и всплывающие окна чат-программ поверх других окон.

Использование дисплея

Некоторые тесты используют дисплей, мышь и клавиатуру тестовой машины и, таким образом, могут завершиться неудачей, если машина используется для чего-то другого в то же время или если несколько тестов выполняются параллельно.

Система непрерывной интеграции (CI) использует выделенные тестовые машины, чтобы избежать этой проблемы, но если у вас нет выделенной тестовой машины, вы можете решить эту проблему, выполнив тесты на втором дисплее.

В Unix также можно запускать тесты на вложенном или виртуальном X-сервере, таком как Xephyr. Например, чтобы запустить весь набор тестов на Xephyr, выполните следующие команды:

Xephyr :1 -ac -screen 1920x1200 >/dev/null 2>&1 &
sleep 5
DISPLAY=:1 icewm >/dev/null 2>&1 &
cd tests/auto
make
DISPLAY=:1 make -k -j1 check

Пользователи двоичных драйверов NVIDIA должны учитывать, что Xephyr может не предоставлять расширения GLX. Возможно, поможет принудительное использование Mesa libGL:

export LD_PRELOAD=/usr/lib/mesa-diverted/x86_64-linux-gnu/libGL.so.1

Однако, когда тесты выполняются на Xephyr, а реальный X-сервер имеет разные версии libGL, кэш диска QML может привести к сбою тестов. Чтобы избежать этого, используйте QML_DISABLE_DISK_CACHE=1.

В качестве альтернативы используйте плагин offscreen:

TESTARGS="-platform offscreen" make check -k -j1

Менеджеры окон

В Unix, как минимум, два автотеста (tst_examples и tst_gestures) требуют, чтобы менеджер окон был запущен. Поэтому, если вы запускаете эти тесты на вложенном X-сервере, вы также должны запустить менеджер окон на этом X-сервере.

Ваш менеджер окон должен быть настроен на автоматическое позиционирование всех окон на дисплее. Некоторые менеджеры окон, такие как Tab Window Manager (twm), имеют режим для ручного позиционирования новых окон, и это препятствует запуску набора тестов без взаимодействия пользователя.

Примечание: Tab Window Manager не подходит для запуска полного набора автотестов Qt, так как автотест tst_gestures заставляет его забыть свою конфигурацию и вернуться к ручному размещению окон.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.0/qttest-best-practices-qdoc.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API