Важно
Перевод - это работа сообщества : ссылка:Вы можете присоединиться. Эта страница в настоящее время переводится |прогресс перевода|.
6. Модульное тестирование
С ноября 2007 года мы требуем, чтобы все новые функции, выходящие в мастер, сопровождались модульным тестированием. Изначально мы ограничили это требование qgis_core, и мы распространим его на другие части кодовой базы, как только люди ознакомятся с процедурами модульного тестирования, описанными в последующих разделах.
6.1. Система тестирования QGIS - обзор
Юнит-тестирование выполняется с помощью комбинации QTestLib (библиотека тестирования Qt) и CTest (фреймворк для компиляции и запуска тестов как часть процесса сборки CMake). Прежде чем перейти к деталям, давайте сделаем обзор процесса:
У вас есть код, который вы хотите протестировать, например класс или функция. Сторонники экстремального программирования считают, что код еще даже не должен быть написан, когда вы начинаете создавать тесты, а затем, по мере реализации кода, вы сможете сразу же проверять каждую новую функциональную часть, которую вы добавляете с помощью теста. На практике вам, скорее всего, придется писать тесты для уже существующего кода в QGIS, поскольку мы начинаем работу с фреймворком тестирования уже после того, как большая часть логики приложения была реализована.
Вы создаете модульный тест. В случае с основной библиотекой это происходит в разделе
<QGIS Source Dir>/tests/src/core
. По сути, тест представляет собой клиент, который создает экземпляр класса и вызывает некоторые методы этого класса. Он будет проверять возврат от каждого метода, чтобы убедиться, что он соответствует ожидаемому значению. Если хотя бы один из вызовов не сработает, модуль завершится неудачей.Вы включаете макросы QtTestLib в свой тестовый класс. Эти макросы обрабатываются компилятором метаобъектов Qt (moc) и превращают ваш тестовый класс в запускаемое приложение.
Вы добавляете раздел в CMakeLists.txt в каталоге tests, который будет собирать ваш тест.
Убедитесь, что в ccmake / cmakesetup включена опция
ENABLE_TESTING
. Это обеспечит реальную компиляцию ваших тестов при вводе make.Вы можете добавить тестовые данные в
<QGIS Source Dir>/tests/testdata
, если ваш тест основан на данных (например, требуется загрузить шейп-файл). Эти тестовые данные должны быть как можно меньше, и по возможности вы должны использовать уже существующие наборы данных. Ваши тесты никогда не должны изменять эти данные на месте, а лишь создавать временную копию, если это необходимо.Скомпилируйте исходные тексты и установите. Для этого используйте обычную процедуру
make && (sudo) make install
.Вы запускаете свои тесты. Обычно для этого достаточно выполнить
make test
после шагаmake install
, хотя мы расскажем о других подходах, которые обеспечивают более тонкий контроль над запуском тестов.
После этого обзора мы перейдем к деталям. Мы уже сделали большую часть настройки за вас в CMake и других местах в дереве исходных текстов, так что вам осталось сделать только самое простое - написать модульные тесты!
6.2. Создание модульного теста
Создать модульный тест очень просто - обычно для этого достаточно создать единственный файл .cpp
(файл .h
не используется) и реализовать все методы теста как приватные методы, возвращающие void. Мы будем использовать простой тестовый класс для QgsRasterLayer
во всем последующем разделе для иллюстрации. По традиции мы будем называть наш тест тем же именем, что и класс, который он тестирует, но с префиксом „Test“. Таким образом, реализация нашего теста будет находиться в файле с именем testqgsrasterlayer.cpp
, а сам класс будет TestQgsRasterLayer
. Сначала мы добавим наш стандартный баннер с копирайтом:
/***************************************************************************
testqgsvectorfilewriter.cpp
--------------------------------------
Date : Friday, Jan 27, 2015
Copyright: (C) 2015 by Tim Sutton
Email: tim@kartoza.com
***************************************************************************
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
***************************************************************************/
Далее мы начинаем собирать инклюды, необходимые для тестов, которые мы планируем запустить. Есть один специальный include, который должен быть у всех тестов:
#include <QtTest/QtTest>
После этого вы просто продолжаете реализовывать свой класс, как обычно, добавляя все необходимые заголовки:
//Qt includes...
#include <QObject>
#include <QString>
#include <QObject>
#include <QApplication>
#include <QFileInfo>
#include <QDir>
//qgis includes...
#include <qgsrasterlayer.h>
#include <qgsrasterbandstats.h>
#include <qgsapplication.h>
Поскольку мы объединяем объявление класса и реализацию в одном файле, следующим идет объявление класса. Мы начинаем с документации doxygen. Каждый тестовый пример должен быть должным образом задокументирован. Мы используем директиву doxygen ingroup, чтобы все юнит-тесты отображались как модуль в сгенерированной документации Doxygen. После этого следует краткое описание юнит-теста, а класс должен наследоваться от QObject и включать макрос Q_OBJECT.
/** \ingroup UnitTests
* This is a unit test for the QgsRasterLayer class.
*/
class TestQgsRasterLayer: public QObject
{
Q_OBJECT
Все наши тестовые методы реализованы как приватные слоты. Фреймворк QtTest будет последовательно вызывать каждый метод приватного слота в тестовом классе. Есть четыре «специальных» метода, которые, если они реализованы, будут вызываться в начале модульного теста (initTestCase
) и в конце модульного теста (cleanupTestCase
). Перед вызовом каждого метода теста вызывается метод init()
, а после вызова каждого метода теста вызывается метод cleanup()
. Эти методы удобны тем, что позволяют выделять и очищать ресурсы перед запуском каждого теста и тестового блока в целом.
private slots:
// will be called before the first testfunction is executed.
void initTestCase();
// will be called after the last testfunction was executed.
void cleanupTestCase(){};
// will be called before each testfunction is executed.
void init(){};
// will be called after every testfunction.
void cleanup();
Затем идут тестовые методы, все из которых не должны принимать никаких параметров и возвращать void. Методы будут вызываться в порядке их объявления. Здесь мы реализуем два метода, которые иллюстрируют два типа тестирования.
В первом случае мы хотим проверить, работают ли различные части класса, и можем использовать подход функционального тестирования. Опять же, экстремальные программисты советуют писать эти тесты до реализации класса. Затем, по мере реализации класса, вы итеративно запускаете свои модульные тесты. По мере реализации класса все больше и больше тестовых функций должны успешно завершаться, и когда весь модульный тест пройдет, ваш новый класс будет завершен, и теперь у вас есть повторяемый способ его проверки.
Как правило, ваши модульные тесты охватывают только публичный API вашего класса, и обычно вам не нужно писать тесты для аксессоров и мутаторов. Если вдруг окажется, что аксессор или мутатор работает не так, как ожидалось, вы обычно реализуете регрессионный тест для проверки этого.
//
// Functional Testing
//
/** Check if a raster is valid. */
void isValid();
// more functional tests here ...
6.2.1. Выполнение регрессионного теста
Далее мы реализуем наши регрессионные тесты. Регрессионные тесты должны быть реализованы таким образом, чтобы воспроизводить условия конкретной ошибки. Например:
Мы получили сообщение по электронной почте о том, что количество ячеек в растрах было смещено на 1, что отбрасывало всю статистику по растровым полосам.
Мы открыли отчет об ошибке (ticket #832)
Мы создали регрессионный тест, который воспроизводил ошибку, используя небольшой тестовый набор данных (растр 10x10).
Мы запустили тест и убедились, что он действительно завершился неудачей (количество клеток составило 99 вместо 100).
Затем мы исправили ошибку и повторно выполнили юнит-тест, и регрессионный тест прошел. Мы зафиксировали регрессионный тест вместе с исправлением ошибки. Теперь, если в будущем кто-то снова нарушит это в исходном коде, мы сможем сразу определить, что код регрессировал.
А еще лучше, если перед тем, как вносить изменения в будущем, мы проведем тесты и убедимся, что наши изменения не приведут к неожиданным побочным эффектам - например, к поломке существующей функциональности.
Есть еще одно преимущество регрессионных тестов - они могут сэкономить ваше время. Если вы когда-нибудь исправляли ошибку, которая предполагала внесение изменений в исходный код, а затем запуск приложения и выполнение ряда сложных шагов для воспроизведения проблемы, то сразу станет ясно, что простое выполнение регрессионного теста перед исправлением ошибки позволит вам эффективно автоматизировать тестирование для ее устранения.
Чтобы реализовать регрессионный тест, вам следует придерживаться соглашения об именовании регрессия<TicketID> для своих тестовых функций. Если для регрессии не существует тикета, сначала создайте его. Такой подход позволит человеку, выполняющему неудачный регрессионный тест, легко перейти к поиску дополнительной информации.
//
// Regression Testing
//
/** This is our second test case...to check if a raster
* reports its dimensions properly. It is a regression test
* for ticket #832 which was fixed with change r7650.
*/
void regression832();
// more regression tests go here ...
Наконец, в объявлении вашего тестового класса вы можете объявить частным образом все члены данных и вспомогательные методы, которые могут понадобиться вашему модульному тесту. В нашем случае мы объявим QgsRasterLayer *
, который может быть использован любым из наших тестовых методов. Растровый слой будет создан в функции initTestCase()
, которая запускается до всех остальных тестов, а затем уничтожен с помощью cleanupTestCase()
, которая запускается после всех тестов. Объявляя вспомогательные методы (которые могут быть вызваны различными тестовыми функциями) в частном порядке, вы можете гарантировать, что они не будут автоматически запущены исполняемым файлом QTest, который создается при компиляции нашего теста.
private:
// Here we have any data structures that may need to
// be used in many test cases.
QgsRasterLayer * mpLayer;
};
На этом объявление нашего класса заканчивается. Реализация просто вставлена в тот же файл ниже. Сначала наши функции init и cleanup:
void TestQgsRasterLayer::initTestCase()
{
// init QGIS's paths - true means that all paths will be inited from prefix
QString qgisPath = QCoreApplication::applicationDirPath ();
QgsApplication::setPrefixPath(qgisPath, TRUE);
#ifdef Q_OS_LINUX
QgsApplication::setPkgDataPath(qgisPath + "/../share/qgis");
#endif
//create some objects that will be used in all tests...
std::cout << "PrefixPATH: " << QgsApplication::prefixPath().toLocal8Bit().data() << std::endl;
std::cout << "PluginPATH: " << QgsApplication::pluginPath().toLocal8Bit().data() << std::endl;
std::cout << "PkgData PATH: " << QgsApplication::pkgDataPath().toLocal8Bit().data() << std::endl;
std::cout << "User DB PATH: " << QgsApplication::qgisUserDbFilePath().toLocal8Bit().data() << std::endl;
//create a raster layer that will be used in all tests...
QString myFileName (TEST_DATA_DIR); //defined in CmakeLists.txt
myFileName = myFileName + QDir::separator() + "tenbytenraster.asc";
QFileInfo myRasterFileInfo ( myFileName );
mpLayer = new QgsRasterLayer ( myRasterFileInfo.filePath(),
myRasterFileInfo.completeBaseName() );
}
void TestQgsRasterLayer::cleanupTestCase()
{
delete mpLayer;
}
Приведенная выше функция init иллюстрирует несколько интересных моментов.
Нам нужно было вручную задать путь к данным приложения QGIS, чтобы такие ресурсы, как
srs.db
, были найдены правильно.Во-вторых, это тест, управляемый данными, поэтому нам нужно было предоставить способ общего определения местоположения файла
tenbytenraster.asc
. Это было достигнуто с помощью определения компилятораTEST_DATA_PATH
. Определение создается в конфигурационном файлеCMakeLists.txt
в<QGIS Source Root>/tests/CMakeLists.txt`
и доступно всем модульным тестам QGIS. Если вам нужны тестовые данные для вашего теста, зафиксируйте их в<QGIS Source Root>/tests/testdata
. Сюда следует фиксировать только очень маленькие наборы данных. Если вашему тесту нужно изменить тестовые данные, он должен сначала сделать их копию.
Qt также предоставляет некоторые другие интересные механизмы для тестирования на основе данных, поэтому если вам интересно узнать больше по этой теме, обратитесь к документации Qt.
Далее рассмотрим наш функциональный тест. Тест isValid()
просто проверяет, правильно ли был загружен растровый слой в initTestCase. QVERIFY - это макрос Qt, который можно использовать для оценки условия теста. Есть несколько других макросов, которые Qt предоставляет для использования в ваших тестах, включая:
QCOMPARE ( actual, expected )
QEXPECT_FAIL ( dataIndex, comment, mode )
QFAIL ( message )
QFETCH ( type, name )
QSKIP ( description, mode )
QTEST ( actual, testElement )
QTEST_APPLESS_MAIN ( TestClass )
QTEST_MAIN ( TestClass )
QTEST_NOOP_MAIN ()
QVERIFY2 ( condition, message )
QVERIFY ( condition )
QWARN ( message )
Некоторые из этих макросов полезны только при использовании фреймворка Qt для тестирования на основе данных (подробнее см. в документации по Qt).
void TestQgsRasterLayer::isValid()
{
QVERIFY ( mpLayer->isValid() );
}
Обычно функциональные тесты охватывают весь спектр функциональности публичного API ваших классов, если это возможно. С функциональными тестами покончено, теперь мы можем рассмотреть пример регрессионного теста.
Поскольку проблема в ошибке #832 заключается в неправильном сообщении о количестве ячеек, написание нашего теста сводится к использованию QVERIFY для проверки того, что количество ячеек соответствует ожидаемому значению:
void TestQgsRasterLayer::regression832()
{
QVERIFY ( mpLayer->getRasterXDim() == 10 );
QVERIFY ( mpLayer->getRasterYDim() == 10 );
// regression check for ticket #832
// note getRasterBandStats call is base 1
QVERIFY ( mpLayer->getRasterBandStats(1).elementCountInt == 100 );
}
Когда все функции юнит-тестов реализованы, осталось добавить еще одну вещь в наш тестовый класс:
QTEST_MAIN(TestQgsRasterLayer)
#include "testqgsrasterlayer.moc"
Цель этих двух строк - дать сигнал Qt’s moc, что это QtTest (он сгенерирует метод main, который, в свою очередь, вызовет каждую тестовую функцию. Последняя строка - это include для исходников, сгенерированных MOC. Вы должны заменить testqgsrasterlayer
на имя вашего класса в нижнем регистре.
6.3. Сравнение изображений для тестов рендеринга
Рендеринг изображений в разных средах может давать тонкие различия из-за специфики платформы (например, разных алгоритмов рендеринга и сглаживания шрифтов), из-за шрифтов, доступных в системе, и по другим неясным причинам.
Когда тест рендеринга запускается в Travis и терпит неудачу, найдите ссылку dash в самом низу журнала Travis. По этой ссылке вы перейдете на страницу cdash, где можно увидеть рендеренные и ожидаемые изображения, а также «разницу», которая выделяет красным цветом все пиксели, не совпадающие с эталонным изображением.
Система модульного тестирования QGIS поддерживает добавление изображений «масок», которые используются для указания, когда отрисованное изображение может отличаться от эталонного. Изображение маски - это изображение (с тем же именем, что и эталонное изображение, но с суффиксом _mask.png), которое должно иметь те же размеры, что и эталонное изображение. В изображении-маске значения пикселей указывают, насколько каждый пиксель может отличаться от эталонного изображения, поэтому черный пиксель означает, что пиксель на отрисованном изображении должен точно совпадать с пикселем на эталонном изображении. Пиксель с RGB 2, 2, 2 означает, что отрисованное изображение может отличаться от эталонного на 2 значения RGB, а полностью белый пиксель (255, 255, 255) означает, что пиксель фактически игнорируется при сравнении ожидаемого и отрисованного изображений.
Утилита для создания изображений масок доступна в виде скрипта scripts/generate_test_mask_image.py
. Этот скрипт используется путем передачи ему пути к эталонному изображению (например, tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png
) и пути к вашему отрисованному изображению.
Например
scripts/generate_test_mask_image.py tests/testdata/control_images/annotations/expected_annotation_fillstyle/expected_annotation_fillstyle.png /tmp/path_to_rendered_image.png
Вы можете сократить путь к эталонному изображению, передав вместо него частичную часть имени теста, например
scripts/generate_test_mask_image.py annotation_fillstyle /tmp/path_to_rendered_image.png
(Этот ярлык работает только в том случае, если найдено одно совпадающее эталонное изображение. Если найдено несколько совпадений, необходимо указать полный путь к эталонному изображению).
Скрипт также принимает http-урлы для отрисованного изображения, поэтому вы можете напрямую скопировать url отрисованного изображения со страницы результатов cdash и передать его скрипту.
Будьте внимательны при генерации изображений масок - всегда просматривайте сгенерированное изображение маски и изучайте все белые области на нем. Поскольку эти пиксели игнорируются, убедитесь, что эти белые области не закрывают важные части эталонного изображения - иначе ваш модульный тест будет бессмысленным!
Аналогично, вы можете вручную «закрасить» части маски, если вы намеренно хотите исключить их из теста. Это может быть полезно, например, для тестов, в которых смешаны рендеринг символов и текста (например, тесты легенд), когда модульный тест не предназначен для проверки рендеринга текста, и вы не хотите, чтобы тест был подвержен кросс-платформенным различиям в рендеринге текста.
Для сравнения изображений в модульных тестах QGIS следует использовать класс QgsMultiRenderChecker
или один из его подклассов.
Чтобы повысить устойчивость тестов, приведем несколько советов:
Отключите сглаживание, если можете, так как это минимизирует различия в кроссплатформенном рендеринге.
Убедитесь, что ваши эталонные изображения «крупные»… т.е. не имеют линий шириной 1 px или других мелких деталей, и используйте крупные, жирные шрифты (рекомендуется 14 пунктов или более).
Иногда тесты генерируют изображения немного разного размера (например, тесты рендеринга легенды, где размер изображения зависит от размера рендеринга шрифта - что подвержено кроссплатформенным различиям). Чтобы учесть это, используйте
QgsMultiRenderChecker::setSizeTolerance()
и укажите максимальное количество пикселей, на которое ширина и высота отрисованного изображения отличается от эталонного.Не используйте прозрачные фоны в опорных изображениях (CDash их не поддерживает). Вместо этого используйте
QgsMultiRenderChecker::drawBackground()
, чтобы нарисовать шаблон для фона опорного изображения.Если требуются шрифты, используйте шрифт, указанный в
QgsFontUtils::standardTestFontFamily()
(«QGIS Vera Sans»).
Если travis сообщает об ошибках для новых изображений (например, из-за сглаживания или различий в шрифтах), скрипт parse_dash_results.py может помочь вам при обновлении локальных тестовых масок.
6.4. Добавление вашего модульного теста в CMakeLists.txt
Добавление вашего юнит-теста в систему сборки - это просто редактирование CMakeLists.txt
в каталоге test, клонирование одного из существующих тестовых блоков, а затем замена в нем имени вашего тестового класса. Например:
# QgsRasterLayer test
ADD_QGIS_TEST(rasterlayertest testqgsrasterlayer.cpp)
6.4.1. Объяснение макроса ADD_QGIS_TEST
Мы вкратце расскажем об этих строках, чтобы объяснить, что они делают, но если вам это неинтересно, просто выполните действия, описанные в разделе выше.
MACRO (ADD_QGIS_TEST testname testsrc)
SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})
SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})
ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)
TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)
SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)
ENDMACRO (ADD_QGIS_TEST)
Давайте рассмотрим отдельные строки немного подробнее. Сначала мы определим список источников для нашего теста. Поскольку у нас только один исходный файл (в соответствии с описанной выше методикой, когда объявление и определение класса находятся в одном файле), это простое утверждение:
SET(qgis_${testname}_SRCS ${testsrc} ${util_SRCS})
Поскольку наш тестовый класс должен быть запущен через компилятор мета-объектов Qt (moc), нам нужно добавить пару строк, чтобы это тоже произошло:
SET(qgis_${testname}_MOC_CPPS ${testsrc})
QT4_WRAP_CPP(qgis_${testname}_MOC_SRCS ${qgis_${testname}_MOC_CPPS})
ADD_CUSTOM_TARGET(qgis_${testname}moc ALL DEPENDS ${qgis_${testname}_MOC_SRCS})
Далее мы сообщаем cmake, что он должен создать исполняемый файл из тестового класса. Помните, в предыдущем разделе в последней строке реализации класса мы включили выводы moc непосредственно в наш тестовый класс, так что это даст ему (помимо всего прочего) метод main, чтобы класс можно было скомпилировать как исполняемый файл:
ADD_EXECUTABLE(qgis_${testname} ${qgis_${testname}_SRCS})
ADD_DEPENDENCIES(qgis_${testname} qgis_${testname}moc)
Далее нам нужно указать все библиотечные зависимости. На данный момент классы реализованы с общей зависимостью QT_LIBRARIES, но мы будем работать над тем, чтобы заменить ее на конкретные библиотеки Qt, которые нужны только каждому классу. Конечно, вам также нужно связать соответствующие библиотеки qgis, как того требует ваш юнит-тест.
TARGET_LINK_LIBRARIES(qgis_${testname} ${QT_LIBRARIES} qgis_core)
Далее мы указываем cmake установить тесты в то же место, что и сам двоичный файл qgis. В будущем мы планируем это убрать, чтобы тесты можно было запускать прямо из дерева исходных текстов.
SET_TARGET_PROPERTIES(qgis_${testname}
PROPERTIES
# skip the full RPATH for the build tree
SKIP_BUILD_RPATHTRUE
# when building, use the install RPATH already
# (so it doesn't need to relink when installing)
BUILD_WITH_INSTALL_RPATH TRUE
# the RPATH to be used when installing
INSTALL_RPATH ${QGIS_LIB_DIR}
# add the automatically determined parts of the RPATH
# which point to directories outside the build tree to the install RPATH
INSTALL_RPATH_USE_LINK_PATH true)
IF (APPLE)
# For macOS, the executable must be at the root of the bundle's executable folder
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/qgis_${testname})
ELSE (APPLE)
INSTALL(TARGETS qgis_${testname} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
ADD_TEST(qgis_${testname} ${CMAKE_INSTALL_PREFIX}/bin/qgis_${testname})
ENDIF (APPLE)
Наконец, выше используется ADD_TEST
для регистрации теста в cmake / ctest. Здесь происходит самое интересное - мы регистрируем класс в ctest. Если вы помните, в обзоре, который мы делали в начале этого раздела, мы используем QtTest и CTest вместе. Напомним, что QtTest добавляет главный метод в ваш тестовый модуль и обрабатывает вызов тестовых методов внутри класса. Он также предоставляет некоторые макросы, такие как ``QVERIFY““, которые вы можете использовать для проверки на провал тестов с помощью условий. Результатом модульного теста QtTest является исполняемый файл, который вы можете запустить из командной строки. Однако если у вас есть набор тестов, и вы хотите запускать каждый исполняемый файл по очереди, а еще лучше интегрировать запуск тестов в процесс сборки, мы используем CTest.
6.5. Создание модульного теста
Чтобы собрать юнит-тест, достаточно убедиться, что ENABLE_TESTS=true
в конфигурации cmake. Это можно сделать двумя способами:
Запустите
ccmake ..
(илиcmakesetup ..
под windows) и интерактивно установите флагENABLE_TESTS
наON
.Добавьте флаг командной строки в cmake, например,
cmake -DENABLE_TESTS=true ..
В остальном просто соберите QGIS, как обычно, и тесты тоже должны собраться.
6.6. Запустите свои тесты
Самый простой способ запустить тесты - это сделать их частью обычного процесса сборки:
make && make install && make test
Команда make test
вызовет CTest, который запустит каждый тест, зарегистрированный с помощью директивы ADD_TEST CMake, описанной выше. Типичный вывод команды make test
будет выглядеть следующим образом:
Running tests...
Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
## 13 Testing qgis_applicationtest***Exception: Other
## 23 Testing qgis_filewritertest *** Passed
## 33 Testing qgis_rasterlayertest*** Passed
## 0 tests passed, 3 tests failed out of 3
The following tests FAILED:
## 1- qgis_applicationtest (OTHER_FAULT)
Errors while running CTest
make: *** [test] Error 8
Если тест не сработал, вы можете использовать команду ctest для более детального изучения причин неудачи. Используйте опцию -R
, чтобы указать regex для запуска тестов, и -V
, чтобы получить подробный вывод:
$ ctest -R appl -V
Start processing tests
Test project /Users/tim/dev/cpp/qgis/build
Constructing a list of tests
Done constructing a list of tests
Changing directory into /Users/tim/dev/cpp/qgis/build/tests/src/core
## 13 Testing qgis_applicationtest
Test command: /Users/tim/dev/cpp/qgis/build/tests/src/core/qgis_applicationtest
********* Start testing of TestQgsApplication *********
Config: Using QTest library 4.3.0, Qt 4.3.0
PASS : TestQgsApplication::initTestCase()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
PASS : TestQgsApplication::getPaths()
PrefixPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/../
PluginPATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//lib/qgis
PkgData PATH: /Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis
User DB PATH: /Users/tim/.qgis/qgis.db
QDEBUG : TestQgsApplication::checkTheme() Checking if a theme icon exists:
QDEBUG : TestQgsApplication::checkTheme()
/Users/tim/dev/cpp/qgis/build/tests/src/core/..//share/qgis/themes/default//mIconProjectionDisabled.png
FAIL!: TestQgsApplication::checkTheme() '!myPixmap.isNull()' returned FALSE. ()
Loc: [/Users/tim/dev/cpp/qgis/tests/src/core/testqgsapplication.cpp(59)]
PASS : TestQgsApplication::cleanupTestCase()
Totals: 3 passed, 1 failed, 0 skipped
********* Finished testing of TestQgsApplication *********
-- Process completed
***Failed
## 0 tests passed, 1 tests failed out of 1
The following tests FAILED:
## 1- qgis_applicationtest (Failed)
Errors while running CTest
6.6.1. Выполнение отдельных тестов
Тесты C++ - это обычные приложения. Вы можете запускать их из папки сборки, как любой исполняемый файл.
$ ./output/bin/qgis_dxfexporttest
********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS : TestQgsDxfExport::initTestCase()
PASS : TestQgsDxfExport::testPoints()
PASS : TestQgsDxfExport::testLines()
...
Totals: 19 passed, 4 failed, 0 skipped, 0 blacklisted, 612ms
********* Finished testing of TestQgsDxfExport *********
Эти тесты также принимают аргументы командной строки <https://doc.qt.io/qt-5/qtest-overview.html#qt-test-command-line-arguments>`_. Это позволяет запускать определенное подмножество тестов:
$ ./output/bin/qgis_dxfexporttest testPoints
********* Start testing of TestQgsDxfExport *********
Config: Using QtTest library 5.12.5, Qt 5.12.5 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 9.2.1 20190827 (Red Hat 9.2.1-1))
PASS : TestQgsDxfExport::initTestCase()
PASS : TestQgsDxfExport::testPoints()
PASS : TestQgsDxfExport::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 272ms
********* Finished testing of TestQgsDxfExport *********
6.6.2. Тестовый запуск в песочнице
По умолчанию все временные файлы теста записываются в системный каталог temp (например, /tmp/
в системах Linux или C:\temp
в Windows). Во время выполнения теста в этом каталоге может быть создано множество файлов.
Если вы не хотите путаться с общим системным temp-каталогом (например, на многопользовательском сервере или в случае проблем с правами), вы можете создать свой собственный temp-каталог и указать его ctest
, установив переменную окружения TMPDIR
с вашим новым каталогом.
В Linux вы можете сделать это с помощью:
$ mkdir ~/my_qgis_temp
$ TMPDIR=~/my_qgis_temp ctest
6.6.3. Отладка модульных тестов
Тесты C++
Для модульных тестов C++ QtCreator автоматически добавляет цели запуска, чтобы вы могли запускать их из отладчика.
Если вы перейдете в Проекты и там на вкладку Сборка и выполнение –> Рабочий стол Выполнить, вы также можете указать параметры командной строки, которые позволят запускать подмножество тестов внутри .cpp-файла в отладчике.
Тесты Python
Также можно запускать юнит-тесты Python из QtCreator с помощью GDB. Для этого вам нужно перейти в Проекты и выбрать Run в разделе Сборка и запуск. Затем добавьте новую конфигурацию Запустить
с исполняемым файлом /usr/bin/python3
и аргументами командной строки, установленными на путь к python-файлу юнит-теста, например /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py`
.
Теперь также измените Запуск окружения
и добавьте 3 новые переменные:
Variable |
Значение |
PYTHONPATH |
[build]/output/python/:[build]/output/python/plugins:[source]/tests/src/python |
QGIS_PREFIX_PATH |
[build]/output |
LD_LIBRARY_PATH |
[build]/output/lib |
Замените [сборка]
на каталог сборки, а [источник]
на каталог исходного кода.
6.6.4. Хорошего настроения
На этом мы завершаем этот раздел о написании модульных тестов в QGIS. Мы надеемся, что у вас войдет в привычку писать тесты для проверки новой функциональности и регрессий. Некоторые аспекты тестовой системы (в частности, части CMakeLists.txt
) все еще дорабатываются, чтобы система тестирования работала действительно независимо от платформы.