6. وحدة التجارب¶
منذ نوفمبر 2007، نشترط أن تكون كل ميزة جديدة تُضاف إلى الفرع master مصحوبة باختبار وحدة. وقد قصرنا هذا الشرط في البداية على qgis_core، وسنوسّعه ليشمل أجزاء أخرى من الشيفرة المصدرية بمجرد أن يعتاد المطورون على إجراءات اختبار الوحدات الموضحة في الأقسام التالية.
6.1. إطار اختبار QGIS - نظرة عامة¶
يُجرى اختبار الوحدات باستخدام مزيج من QTestLib (مكتبة الاختبار في Qt) و CTest (إطار لترجمة الاختبارات وتشغيلها كجزء من عملية البناء في CMake). لنأخذ نظرة عامة على العملية قبل الخوض في التفاصيل:
هناك شيفرة تريد اختبارها، مثل صنف أو دالة. يقترح أنصار البرمجة القصوى (Extreme programming) ألّا تكون الشيفرة قد كُتبت أصلًا عندما تبدأ بناء اختباراتك، ثم أثناء تنفيذ شيفرتك يمكنك التحقق فورًا من كل جزء وظيفي جديد تضيفه باستخدام اختبارك. أما عمليًا فستحتاج على الأرجح إلى كتابة اختبارات لشيفرة موجودة مسبقًا في QGIS لأننا بدأنا بإطار الاختبار بعد وقت طويل من تنفيذ جزء كبير من منطق التطبيق.
تُنشئ اختبار وحدة. ويتم ذلك تحت
<QGIS Source Dir>/tests/src/coreفي حالة المكتبة الأساسية (core). والاختبار في الأساس عميل يُنشئ نسخة (instance) من صنف ويستدعي بعض دوال ذلك الصنف. وسيتحقق من القيمة المعادة من كل دالة للتأكد من مطابقتها للقيمة المتوقعة. وإذا فشل أي استدعاء، فستفشل الوحدة.يمكنك تضمين وحدات الماكرو اختبار Qt اختبار في صف الاختبار. تتم معالجة هذا الماكرو بواسطة برنامج التحويل البرمجي كائن meta (moc) Qt ويقوم بتوسيع فئة الاختبار الخاص بك إلى تطبيق
كنك إضافة قسم إلى قوائم make .txt في دليل الاختبارات الذي سيقوم ببناء الاختبار.
تتأكد من تفعيل
ENABLE_TESTINGفي ccmake / cmakesetup. سيضمن ذلك ترجمة اختباراتك فعلًا عند كتابة make.تضيف اختياريًا بيانات اختبار إلى
<QGIS Source Dir>/tests/testdataإذا كان اختبارك معتمدًا على البيانات (مثلًا يحتاج إلى تحميل ملف shapefile). يجب أن تكون بيانات الاختبار هذه صغيرة قدر الإمكان، وعليك كلما أمكن استخدام مجموعات البيانات الموجودة هناك بالفعل. ويجب ألا تعدّل اختباراتك هذه البيانات في مكانها أبدًا، بل تنشئ نسخة مؤقتة في مكان ما إن لزم الأمر.تترجم مصادرك وتثبّتها. افعل ذلك باستخدام الإجراء المعتاد
make && (sudo) make install.تشغّل اختباراتك. ويتم ذلك عادةً ببساطة بتنفيذ
make testبعد خطوةmake install، مع أننا سنشرح أساليب أخرى توفر تحكمًا أدق في تشغيل الاختبارات.
حسنًا، مع وضع هذه النظرة العامة في الاعتبار، سنخوض في بعض التفاصيل. لقد أنجزنا بالفعل كثيرًا من الإعدادات نيابةً عنك في CMake وأماكن أخرى في شجرة المصدر، لذا كل ما عليك فعله هو الأجزاء السهلة: كتابة اختبارات الوحدات!
6.2. إنشاء اختبار وحدة¶
إنشاء اختبار وحدة أمر سهل، فعادةً ستفعل ذلك بمجرد إنشاء ملف .cpp واحد (لا يُستخدم ملف .h) وتنفيذ جميع دوال الاختبار كدوال خاصة (private) تُعيد 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.
*
***************************************************************************/
بعد ذلك نبدأ بعبارات التضمين (includes) اللازمة للاختبارات التي ننوي تشغيلها. وهناك تضمين خاص واحد يجب أن يحتويه كل اختبار:
#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. يجب توثيق كل حالة اختبار توثيقًا سليمًا. ونستخدم التوجيه ingroup في doxygen بحيث تظهر جميع اختبارات الوحدات (UnitTests) كوحدة واحدة في توثيق Doxygen المُولَّد. ويلي ذلك وصف موجز لاختبار الوحدة، ويجب أن يرث الصنف من QObject وأن يتضمن الماكرو Q_OBJECT.
/** \ingroup UnitTests
* This is a unit test for the QgsRasterLayer class.
*/
class TestQgsRasterLayer: public QObject
{
Q_OBJECT
تُنفَّذ جميع دوال الاختبار لدينا كمنافذ خاصة (private slots). وسيستدعي إطار 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. وستُستدعى الدوال بترتيب الإعلان عنها. ننفّذ هنا دالتين توضحان نوعين من الاختبار.
في الحالة الأولى نريد اختبار ما إذا كانت أجزاء الصنف المختلفة تعمل بشكل عام، ويمكننا استخدام أسلوب الاختبار الوظيفي. ومرة أخرى، يدعو أنصار البرمجة القصوى إلى كتابة هذه الاختبارات قبل تنفيذ الصنف. ثم أثناء تقدمك في تنفيذ صنفك تشغّل اختبارات الوحدة بشكل متكرر. ويجب أن يكتمل المزيد والمزيد من دوال الاختبار بنجاح مع تقدم العمل على تنفيذ الصنف، وعندما ينجح اختبار الوحدة بالكامل يكون صنفك الجديد قد اكتمل ومعه طريقة قابلة للتكرار للتحقق من صحته.
عادةً تغطي اختبارات الوحدة الواجهة البرمجية العامة لصنفك فقط، ولا تحتاج في العادة إلى كتابة اختبارات لدوال الوصول والتعديل (accessors و mutators). وإذا حدث أن دالة وصول أو تعديل لا تعمل كما هو متوقع، فعادةً تنفّذ اختبار انحدار للتحقق من ذلك.
//
// Functional Testing
//
/** Check if a raster is valid. */
void isValid();
// more functional tests here ...
6.2.1. تنفيذ اختبار انحدار¶
بعد ذلك ننفّذ اختبارات الانحدار (regression tests). ويجب تنفيذ اختبارات الانحدار لإعادة إنتاج ظروف خلل (bug) معين. على سبيل المثال:
تلقينا بلاغًا بالبريد الإلكتروني بأن عدد الخلايا في البيانات الشبكية خاطئ بمقدار 1، مما أفسد جميع الإحصاءات لنطاقات البيانات الشبكية.
فتحنا تقرير خلل (التذكرة #832)
أنشأنا اختبار انحدار يعيد إنتاج الخلل باستخدام مجموعة بيانات اختبار صغيرة (بيانات شبكية بحجم 10x10).
شغّلنا الاختبار وتحققنا من أنه فشل بالفعل (كان عدد الخلايا 99 بدلًا من 100).
ثم أصلحنا الخلل وأعدنا تشغيل اختبار الوحدة فنجح اختبار الانحدار. وأودعنا اختبار الانحدار مع إصلاح الخلل. والآن إذا أفسد أي شخص هذا الجزء في الشيفرة المصدرية مرة أخرى مستقبلًا، يمكننا أن نكتشف فورًا أن الشيفرة قد تراجعت.
والأفضل من ذلك، أن تشغيل اختباراتنا قبل إيداع أي تغييرات مستقبلًا سيضمن ألا يكون لتغييراتنا آثار جانبية غير متوقعة، مثل تعطيل وظائف موجودة.
لاختبارات الانحدار فائدة أخرى - فهي قد توفّر عليك الوقت. فإذا سبق لك إصلاح خلل تطلّب إجراء تغييرات على الشيفرة المصدرية، ثم تشغيل التطبيق وتنفيذ سلسلة من الخطوات المعقدة لإعادة إنتاج المشكلة، فسيتضح لك فورًا أن مجرد كتابة اختبار الانحدار قبل إصلاح الخلل سيتيح لك أتمتة اختبار حل الخلل بطريقة فعّالة.
لتنفيذ اختبار الانحدار، يجب اتباع اصطلاح التسمية regression<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 ...
أخيرًا، يمكنك في إعلان صنف الاختبار أن تعلن بشكل خاص (private) عن أي أعضاء بيانات ودوال مساعدة قد يحتاجها اختبار الوحدة. في حالتنا سنعلن عن QgsRasterLayer * يمكن استخدامه في أي من دوال الاختبار. وستُنشأ الطبقة الشبكية في الدالة initTestCase() التي تعمل قبل أي اختبارات أخرى، ثم تُتلف باستخدام cleanupTestCase() التي تعمل بعد جميع الاختبارات. وبالإعلان عن الدوال المساعدة (التي قد تستدعيها دوال اختبار مختلفة) بشكل خاص، تضمن ألا يشغّلها تلقائيًا الملف التنفيذي لـ QTest الذي يُنشأ عند ترجمة اختبارنا.
private:
// Here we have any data structures that may need to
// be used in many test cases.
QgsRasterLayer * mpLayer;
};
هذا ينهي تصريح طبقتنا. يتم ببساطة تضمين التطبيق في نفس الملف لأسفل. أولاً لدينا وظائف التنظيف والتنظيف:
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 )
بعض وحدات الماكرو هذه مفيدة فقط عند استخدام إطار عمل كيو تي للإختبار المبني على البيانات (انظر مستندات كيو تي لمزيد من التفاصيل).
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"
الغرض من هذين السطرين هو إبلاغ أداة moc في Qt بأن هذا اختبار QtTest (ستولّد دالة main تستدعي بدورها كل دالة اختبار). والسطر الأخير هو تضمين المصادر المولَّدة بواسطة MOC. ويجب أن تستبدل testqgsrasterlayer باسم صنفك بأحرف صغيرة.
6.3. مقارنة الصور في اختبارات العرض¶
قد يُنتج عرض الصور في بيئات مختلفة فروقًا طفيفة بسبب التطبيقات الخاصة بكل منصة (مثل اختلاف خوارزميات عرض الخطوط وتنعيم الحواف)، أو بسبب الخطوط المتوفرة على النظام، أو لأسباب غامضة أخرى.
عندما يُشغَّل اختبار عرض على Travis ويفشل، ابحث عن رابط dash في أسفل سجل Travis تمامًا. سينقلك هذا الرابط إلى صفحة cdash حيث يمكنك رؤية الصور المعروضة مقابل الصور المتوقعة، مع صورة "الفرق" التي تُبرز باللون الأحمر أي بكسلات لا تطابق الصورة المرجعية.
يدعم نظام اختبار الوحدات في QGIS إضافة صور "قناع" (mask)، تُستخدم للإشارة إلى المواضع التي قد تختلف فيها الصورة المعروضة عن الصورة المرجعية. وصورة القناع صورة (بنفس اسم الصورة المرجعية لكن مع اللاحقة _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 أو أحد الأصناف المشتقة منه.
لتحسين متانة الاختبارات، إليك بعض النصائح:
عطّل تنعيم الحواف (antialiasing) إن أمكن، لأن ذلك يقلل فروق العرض بين المنصات.
تأكد من أن صورك المرجعية "ممتلئة"... أي لا تحتوي على خطوط بعرض 1 بكسل أو تفاصيل دقيقة أخرى، واستخدم خطوطًا كبيرة وعريضة (يوصى بحجم 14 نقطة أو أكثر).
أحيانًا تولّد الاختبارات صورًا بأحجام مختلفة قليلًا (مثل اختبارات عرض مفتاح الخريطة، حيث يعتمد حجم الصورة على حجم عرض الخط، وهو عرضة للفروق بين المنصات). ولمراعاة ذلك، استخدم
QgsMultiRenderChecker::setSizeTolerance()وحدد أقصى عدد من البكسلات يمكن أن يختلف به عرض الصورة المعروضة وارتفاعها عن الصورة المرجعية.لا تستخدم خلفيات شفافة في الصور المرجعية (لا يدعمها CDash). بدلًا من ذلك، استخدم
QgsMultiRenderChecker::drawBackground()لرسم نمط رقعة الشطرنج لخلفية الصورة المرجعية.عند الحاجة إلى خطوط، استخدم الخط المحدد في
QgsFontUtils::standardTestFontFamily()("QGIS Vera Sans").
إذا أبلغ travis عن أخطاء في الصور الجديدة (مثلًا بسبب فروق تنعيم الحواف أو الخطوط)، فيمكن أن يساعدك السكربت parse_dash_results.py عند تحديث أقنعة الاختبار المحلية.
6.4. إضافة اختبار الوحدة الخاصة بك إلى CMakeLists.tx¶
إضافة اختبار الوحدة إلى نظام البناء أمر بسيط يقتصر على تحرير الملف CMakeLists.txt في مجلد الاختبارات، واستنساخ إحدى كتل الاختبار الموجودة، ثم وضع اسم صنف اختبارك فيها. على سبيل المثال:
# 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 دالة main إلى وحدة الاختبار ويتولى استدعاء دوال الاختبار داخل الصنف. كما يوفر بعض وحدات الماكرو مثل 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 التي ستشغّل كل اختبار سُجّل باستخدام توجيه CMake المسمى ADD_TEST الموضح أعلاه. ويبدو المخرج النموذجي للأمر 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 *********
تقبل هذه الاختبارات أيضًا وسائط سطر الأوامر. وهذا يتيح تشغيل مجموعة فرعية محددة من الاختبارات:
$ ./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. تشغيل الاختبارات في بيئة معزولة¶
افتراضيًا تُكتب جميع الملفات المؤقتة للاختبارات في المجلد المؤقت للنظام (مثل /tmp/ على أنظمة Linux أو C:\temp على Windows). وقد يُنشأ كثير من الملفات في هذا المجلد أثناء تشغيل الاختبارات.
إذا كنت لا تريد الخلط مع المجلد المؤقت العام للنظام (مثلًا على خادم متعدد المستخدمين أو في حالة مشكلات الصلاحيات)، فيمكنك إنشاء مجلدك المؤقت الخاص وتحديده لـ ctest بتعيين متغير البيئة TMPDIR إلى مجلدك الجديد.
على Linux يمكنك فعل ذلك باستخدام:
$ mkdir ~/my_qgis_temp
$ TMPDIR=~/my_qgis_temp ctest
6.6.3. تنقيح اختبارات الوحدة¶
اختبارات C++¶
بالنسبة لاختبارات الوحدة في C++، يضيف QtCreator أهداف تشغيل تلقائيًا، لذا يمكنك تشغيلها من المنقّح.
إذا انتقلت إلى Projects ومنها إلى تبويب Build & Run --> Desktop Run، يمكنك أيضًا تحديد معاملات سطر الأوامر التي تتيح تشغيل مجموعة فرعية من الاختبارات داخل ملف .cpp في المنقّح.
اختبارات Python¶
يمكن أيضًا تشغيل اختبارات الوحدة في Python من QtCreator باستخدام GDB. لذلك تحتاج إلى الانتقال إلى Projects واختيار Run ضمن Build & Run. ثم أضف Run configuration جديدًا بالملف التنفيذي /usr/bin/python3 مع ضبط وسائط سطر الأوامر على مسار ملف python لاختبار الوحدة، مثل /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattributeformeditorwidget.py.
الآن غيّر أيضًا Run Environment وأضف 3 متغيرات جديدة:
المتغير |
القيمة |
|---|---|
PYTHONPATH |
[build]/output/python/:[build]/output/python/plugins:[source]/tests/src/python |
QGIS_PREFIX_PATH |
[build]/output |
LD_LIBRARY_PATH |
[build]/output/lib |
استبدل [build] بمجلد البناء لديك و [source] بمجلد المصدر لديك.
6.6.4. استمتع¶
بهذا نختم هذا القسم حول كتابة اختبارات الوحدة في QGIS. نأمل أن تعتاد كتابة الاختبارات لاختبار الوظائف الجديدة والتحقق من حالات الانحدار. وما زال العمل جاريًا على بعض جوانب نظام الاختبار (خاصة أجزاء CMakeLists.txt) ليعمل إطار الاختبار بطريقة مستقلة عن المنصة فعلًا.