6. ユニットテスト

2007年11月の時点で、私たちはマスターに入るすべての新機能は、ユニットテストを伴うことを要求しています。当初は私たちはこの要件はqgis_coreに制限してきましたが、皆様が次のセクションで説明するユニットテストのための手続きに精通した後は、この要件をコードベースの他の部分に拡張するつもりです。

6.1. QGISのテストフレームワーク - 概要

ユニットテストは、QTestLib(Qtのテストライブラリ)とCTest(CMakeのビルドプロセスの一部としてテストをコンパイルして実行するためのフレームワーク)の組み合わせを使って行われます。詳細に入る前に、プロセスの概要を説明しましょう:

  1. テストしたいコード、たとえばクラスや関数があるとします。エクストリーム・プログラミングの提唱者たちは、コードは、テストを作る前に書いてはならない、そうすればコードを実装したときに、追加した新しい機能の箇所をテストを使ってすぐに検証することができる、と提唱しています。実際には、多くのアプリケーションロジックが実装された後にテストフレームワークを開始するため、QGISの既存のコードに対してテストを記述する必要があるでしょう。

  2. ユニットテストを作成します。これはcore libの場合 <QGIS Source Dir>/tests/src/core の下で起こります。テストは、基本的には、クラスのインスタンスを作成し、そのクラスにいくつかのメソッドを呼び出すクライアントです。それは各メソッドからの戻りをチェックし、期待値と一致することを確認します。呼び出しのいずれかに障害が発生した場合、そのユニットは失敗します。

  3. テストクラスにQtTestLibマクロを含めます。このマクロは、Qtのメタオブジェクトコンパイラ(MOC)によって処理され、実行可能なアプリケーションにテストクラスを拡張します。

  4. テストを構築するtestsディレクトリにCMakeLists.txtにセクションを追加します。

  5. ENABLE_TESTING がccmake/cmakesetupで有効になっていることを確認します。これは、makeと入力すると、テストが実際にコンパイルされることを保証します。

  6. テストがデータ駆動型の場合(シェープファイルを読み込む必要があるなど)、オプションで <QGIS Source Dir> /tests/testdata にテストデータを追加することもできます。これらのテストデータはできるだけ小さくし、可能な限り既存のデータセットを使用します。テストは決してその場でこのデータを変更してはならず、必要な場合は一時的なコピーをどこかに作るべきです。

  7. ソースをコンパイルしてインストールします。これは通常の make && (sudo) make install 手順を使用して実行します。

  8. テストを実行します。通常は make install の後に make test を実行するだけですが、テストの実行をより細かく制御する他の方法についても説明します。

概要を理解した上で、もう少し詳しく説明します。すでにCMakeやソースツリーの他の場所で、あなたのために多くの設定を行っているので、あなたがする必要があるのは簡単なこと、つまりユニットテストを書くことだけです!

6.2. ユニットテストの作成

ユニットテストの作成は簡単で、通常は .cpp ファイルを1つ作成し( .h ファイルは使用しない)、すべてのテストメソッドを void を返す public メソッドとして実装するだけです。この後のセクションで QgsRasterLayer の簡単なテストクラスを使って説明します。テストの名前は、慣例として、テストするクラスと同じ名前にプレフィックス 'Test' を付けます。ですから、テストの実装は testqgsrasterlayer.cpp というファイルに記述し、クラス名は TestQgsRasterLayer となります。最初に標準的なコピーライトのバナーを追加します:

/***************************************************************************
 testqgsvectorfilewriter.cpp
 --------------------------------------
  Date : Friday, Jan 27, 2015
  Copyright: (C) 2015 by Tim Sutton
  Email: [email protected]
 ***************************************************************************
 *
 * 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 <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のドキュメントのモジュールとして表示されるように、doxygenのingroupディレクティブを使用します。その後にユニットテストの短い説明が来て、このクラスはQObjectを継承し、Q_OBJECTマクロをインクルードする必要があります。

/** \ingroup UnitTests
 * This is a unit test for the QgsRasterLayer class.
 */

class TestQgsRasterLayer: public QObject
{
    Q_OBJECT

すべてのテストメソッドはプライベートスロットとして実装されています。QtTestフレームワークは、テストクラスの各プライベートスロットメソッドを順次呼び出します。もし4つの '特別な' メソッドが実装されていれば、ユニットテストの開始時 (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を返します。メソッドは宣言順に呼び出される。ここでは、2種類のテストを説明する2つのメソッドを実装します。

最初のケースでは、クラスのさまざまな部分が機能しているかどうかを一般的にテストします。関数的なテスト方法のアプローチが使えます。繰り返しますが、エクストリームプログラマはクラスを実装する前にこれらのテストを書くことを推奨します。そして、クラスの実装を進めながら、ユニットテストを繰り返し実行します。クラス実装の作業が進むにつれて、より多くのテスト関数が正常に完了するはずです。そして、すべてのユニットテストがパスしたら、新しいクラスは完成し、それを検証するための再現可能な方法が完成したことになります。

通常、ユニットテストはクラスのパブリック API のみを対象とし、accessorやmutatorのテストを書く必要はありません。もしaccessorやmutatorが期待通りに動作しないようなことがあれば、通常は regression test を実装してそれをチェックします。

//
// Functional Testing
//

/** Check if a raster is valid. */
void isValid();

// more functional tests here ...

6.2.1. 回帰テストを実装する

次に回帰テストを実装します。回帰テストは、特定のバグの状態を再現するように実装する必要があります。例えば :

  1. ラスタバンドのセルカウントが1ずれたため、ラスタバンドの統計がすべて狂ってしまったという報告をメールで受け取りました。

  2. バグレポート (ticket #832) を開きました。

  3. 小さなテストデータセット(10x10のラスター)を使ってバグを再現する回帰テストを作成しました。

  4. テストを実行し、確かに失敗することを確認しました(セルカウントは100ではなく99でした)。

  5. そしてバグを修正し、ユニットテストを再実行したところ、回帰テストはパスしました。バグ修正と一緒に回帰テストもコミットしました。これで、将来誰かがソースコードでこれを再び壊した場合、コードが回帰したことをすぐに特定できるようになりました。

    さらに、今後変更をコミットする前に、テストを実行することで、変更が予期せぬ副作用(既存の機能を壊すなど)をもたらさないことを確認できます。

回帰テストへの1つの以上の利点があります - それらは時間を節約できます。ソースに変更を加えてからアプリケーションを実行して問題を再現するために複雑な一連のステップを実行することに関わるバグを修正したことがあれば、バグを修正する前に回帰テストを実装するだけで、効率的な方法でバグの解決のためのテストを自動化できることがすぐに明らかになるでしょう。

回帰テストを実装するには、テスト関数の命名規則 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 ...

最後にテストクラスの宣言で、ユニットテストが必要とするデータメンバーやヘルパーメソッドを非公開で宣言します。この例ではどのテストメソッドでも使える 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 path 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関数はいくつかの興味深い物事を示しています。

  1. srs.db などのリソースが適切に見つかるように、QGISアプリケーションのデータパスを手動で設定する必要がありました。

  2. 次に、これはデータ駆動型のテストなので、 tenbytenraster.asc ファイルを一般的に見つける方法を提供する必要がありました。これはコンパイラ定義の TEST_DATA_PATH を使うことで実現しました。この定義は <QGIS Source Root>/tests/CMakeLists.txt にある 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のすべての機能範囲をカバーするでしょう。私たちの機能テストは out the way、回帰テストの例を見ることができます。

バグ#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"

この2行の目的は、QtのmocにこれがQtTestであることを知らせることです(mainメソッドを生成し、それが順番に各テスト関数を呼び出します)。最後の行はMOCが生成するソースのインクルードです。testqgsrasterlayer を小文字のクラス名に置き換えてください。

6.3. レンダリングテスト用に画像を比較する

異なる環境で画像をレンダリングすると、プラットフォーム固有の実装(フォントレンダリングやアンチエイリアシングアルゴリズムの違いなど)や、システムで利用可能なフォント、その他の不明瞭な理由により、微妙な違いが生じることがあります。

Travis でレンダリングテストを実行して失敗した場合、Travis ログの一番下にある dash リンクを探します。このリンクから cdash ページに移動し、レンダリングされた画像と期待される画像の比較や、参照画像と一致しなかったピクセルを赤くハイライトする「差分」画像を見ることができます。

QGISユニットテストシステムは、レンダリング画像が参照画像と異なる可能性がある場合に使用される「マスク」画像の追加をサポートしています。マスク画像は画像(参照画像と同じ名前ですが、**_mask.png**という接尾辞が付きます)で、参照画像と同じ寸法である必要があります。マスク画像のピクセル値は、個々のピクセルが参照画像とどの程度異なってもよいかを示します。したがって、黒いピクセルは、レンダリング画像のピクセルが参照画像の同じピクセルと完全に一致しなければならないことを示します。RGB 2, 2, 2のピクセルは、レンダリング画像が参照画像とRGB値で最大2まで異なる可能性があることを意味し、完全な白ピクセル(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

(このショートカットは、一致する参照画像が1つだけ見つかった場合にのみ機能します。一致する画像が複数見つかった場合は、参照画像へのフルパスを入力する必要があります。)

スクリプトはレンダリング画像のhttp urlも受け付けるので、cdashの結果ページからレンダリング画像のurlを直接コピーしてスクリプトに渡すことができます。

マスク画像を生成するときには注意が必要です。生成されたマスク画像を見て、画像内の白い部分を常に確認する必要があります。これらのピクセルは無視されるので、これらの白いイメージが参照イメージの重要な部分をカバーしていないことを確認してください。そうでなければ、ユニットテストは無意味なものになってしまいます!

同様に、意図的にテストから除外したい場合は、マスクの一部を手動で「ホワイトアウト」することができます。これは、シンボルとテキストのレンダリングが混在するテスト(凡例のテストなど)で、ユニットテストがレンダリングされたテキストをテストするように設計されておらず、テストがクロスプラットフォームのテキストレンダリングの違いに左右されたくない場合などに便利です。

QGISのユニットテストで画像を比較するには、クラス QgsMultiRenderChecker またはそのサブクラスのいずれかを使用する必要があります。

テストの堅牢性を高めるためのヒントをいくつか紹介します:

  1. クロスプラットフォームでのレンダリングの違いを最小限に抑えるため、可能であればアンチエイリアスを無効にしてください。

  2. 参考画像は「がっしりした」ものにしてください...つまり、1px幅の線やその他の細かい地物がないようにし、大きくて太いフォント(14ポイント以上を推奨)を使ってください。

  3. 時々、テストはわずかに異なるサイズの画像を生成します(例えば、凡例のレンダリングテストでは、画像サイズはフォントのレンダリングサイズに依存し、クロスプラットフォームの違いに左右されます)。 これを考慮するには、QgsMultiRenderChecker::setSizeTolerance() を使用して、レンダリング画像の幅と高さが参照画像と異なる最大ピクセル数を指定します。

  4. 参照画像に透明な背景を使用しないでください(CDashはそれをサポートしていません)。その代わりに、 QgsMultiRenderChecker::drawBackground() を使用して、参照画像の背景にチェックボードパターンを描画してください。

  5. フォントが必要な場合は、``QgsFontUtils::standardTestFontFamily()``("QGIS Vera Sans")で指定されたフォントを使用します。

travis が新しい画像に対してエラーを報告した場合 (例えば、アンチエイリアスやフォントの違いによるもの)、 parse_dash_results.py スクリプトは、ローカルのテストマスクを更新するときに役立ちます。

6.4. CMakeLists.txtにユニットテストを追加する

ユニットテストをビルドシステムに追加するのは単純で、testディレクトリの 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)

個々の行をもう少し詳しく見てみましょう。まず、テストのソースのリストを定義します。ソースファイルは1つしかない(クラス宣言と定義が同じファイルにある上述の方法に従って)ので、単純な記述になります:

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の出力をテストクラスに直接インクルードし、(とりわけ)メインメソッドが与えられ、クラスを実行ファイルとしてコンパイルできるようになるのを思い出してください:

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)

次に、テストをqgisバイナリと同じ場所にインストールするようcmakeに指示します。これは将来的に削除する予定なので、テストはソースツリーの中から直接実行できるようにします。

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 メソッドを追加し、クラス内でテストメソッドを呼び出す処理を行います。QtTest には QVERIFY のようなマクロも用意されており、条件を使ってテストの失敗をテストすることができます。QtTest のユニットテストの出力は、コマンドラインから実行できる実行ファイルです。しかし、一連のテストがあり、各実行ファイルを順番に実行したい場合、さらにテストの実行をビルドプロセスに統合したい場合は、CTest を使用します。

6.5. ユニットテストをビルドする

ユニットテストをビルドするには、cmakeの設定で ENABLE_TESTS=true を確認するだけが必要です。これを行うには2つの方法があります:

  1. ccmake.. (またはウィンドウズの下では cmakesetup.. )を実行し、対話的に ENABLE_TESTS フラグを ON に設定します。

  2. 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 オプションを、冗長な出力を得るためには -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. ユニットテストをデバッグする

C++ テスト

C++ ユニットテストでは、QtCreator が自動的に実行ターゲットを追加するので、デバッガから開始できます。

Projects から Build & Run --> Desktop Run タブを開くと、デバッガの.cppファイル内でテストのサブセットを実行するためのコマンドラインパラメータを指定することもできます。

Pythonテスト

GDBを使ってQtCreatorからPythonのユニットテストを開始することも可能です。そのためには Projects に行き、 Build & Run の下にある Run を選択する必要があります。次に、実行ファイル /usr/bin/python3 とコマンドライン引数をユニットテストのPythonファイルのパスに設定した新しい Run configuration を追加します。 例 /home/user/dev/qgis/QGIS/tests/src/python/test_qgsattrbuteformeditorwidget.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.3. 楽しむ

これでQGISのユニットテストの書き方についてのこのセクションは終わりです。新しい機能をテストしたり、リグレッションをチェックするためにテストを書く習慣をつけていただければと思います。テストシステムのいくつかの側面(特に CMakeLists.txt の部分)は、テストフレームワークが真にプラットフォームに依存しない方法で動作するように、まだ作業中です。