Qt テストのベストプラクティス

バグフィックスと新機能のために Qt テストを追加することを推奨します。バグを修正しようとする前に、修正前に失敗してバグを示し、修正後に合格するリグレッションテスト(理想的には自動テスト)を追加してください。新機能の開発中は、それが意図したとおりに動くかどうかを検証するテストを追加する。

一連のコーディング標準に準拠することで、Qt の自動テストがすべての環境で確実に動作する可能性が高くなります。例えば、ディスクからデータを読み込む必要があるテストがあります。これをどのように行うかについての標準が設定されていなければ、いくつかのテストは移植性がありません。たとえば、テストデータファイルがカレント作業ディレクトリにあると仮定するテストは、 ソース内ビルドでのみ動作します。シャドウビルド(ソースディレクトリ外)では、そのテストはデータを見つけることができません。

以下のセクションでは、Qt のテストを書くためのガイドラインを説明します:

一般原則

以下のセクションでは、ユニットテストを書くための一般的なガイドラインを示します:

テストの検証

テストを書いて、修正や新機能と一緒に新しいブランチにコミットします。そして、そのブランチに新しいテストのテストファイルをチェックアウトします。こうすることで、前のブランチでテストが失敗したことを検証し、 実際にバグを発見したり新機能をテストしたりできるようになります。

たとえば、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 ブランチにチェックアウトします。残りのソースツリーは 5.14 のままです。

  8. テストをビルドして実行し、5.14 上で失敗することを確認します。
  9. これで fix ブランチに戻ることができます:git checkout fix-branch
  10. あるいは、作業ツリーを 5.14 のクリーンな状態に戻すこともできます:git checkout HEAD -- tests/auto/corelib/time/qdatetime/tst_qdatetime.cpp

変更をレビューしているときに、このワークフローを応用して、その変更が本当に問題を修正するためのテストを含んでいるかどうかをチェックすることができます。

テスト関数にわかりやすい名前をつける

テストケースに名前をつけることは重要です。テスト名は、テスト実行の失敗レポートに表示されます。データ駆動型のテストでは、データ行の名前も失敗レポートに表示されます。この名前によって、レポートを読む人は、何が間違っているのかを最初に知ることができます。

テスト関数の名前は、その関数が何をテストしようとしているのかを明らかにするものでなければなりません。バグ追跡の識別子をそのまま使ってはいけません。 バグ追跡者が交代すると識別子が古くなってしまうからです。また、バグトラッカーによってはすべてのユーザーがアクセスできるわけではありません。バグ報告が後でテストコードを読む人にとって有益なものである場合は、 テストの関連する部分と一緒にコメントで言及することができます。

同様に、データ駆動型のテストを書くときには、 テストケースに説明的な名前をつけましょう。単にテストケースに番号を振ったり、 バグ追跡用の識別子を使ったりしてはいけません。テストの出力を読んだ人は、その番号や識別子が何を意味するのかわからなくなってしまいます。関連する場合は、バグ追跡用の識別子について言及したコメントをテスト行に追加することができます。間隔をあける文字や、 テストを実行するコマンドラインシェルにとって重要な文字は避けたほうがよいでしょう。こうすることで、テストプログラムへのコマンドラインでテストとタグを指定するのが簡単になります。

自己完結型のテスト関数を書く

テストプログラム内では、テスト関数は互いに独立したものであるべきで、 以前のテスト関数が実行されたことに依存してはいけません。これは、tst_foo testname を使ってテスト関数を実行することで確認できます。

テスト対象のクラスのインスタンスを、複数のテストで再利用しないでください。テストのインスタンス (たとえばウィジェットなど) は、テストのメンバ変数にすべきではありません。 テストが失敗した場合でも適切にクリーンアップできるように、 スタック上でインスタンス化するのが望ましいです。

フルスタックのテスト

API がプラグイン可能あるいはプラットフォーム固有のバックエンドで実装されている場合、バックエンドに至るまでコードパスをカバーするテストを書くようにしましょう。モックバックエンドを使って上位レイヤーのAPIパーツをテストするのは、APIレイヤーのエラーをバックエンドから切り離す良い方法だが、実際のデータを使って実際の実装を実行するテストを補完するものだ。

テストを素早く完了させる

テストは、不必要に繰り返したり、不適切に大量のテストデータを使ったり、不必要なアイドル時間を導入したりして、時間を浪費してはいけません。

これは特にユニットテストに当てはまり、ユニットテストの実行時間が1秒増えるごとに、複数のターゲットにまたがるブランチのCIテストに時間がかかるようになる。単体テストは負荷テストや信頼性テストとは別物であり、そこでは大量のテストデータと長時間のテスト実行が期待される。

通常、同じテストを複数回実行するベンチマークテストは、別のtests/benchmarks ディレクトリに配置し、機能ユニットテストと混ぜてはいけません。

データ駆動型テストの使用

データ駆動テストは、後のバグ報告で見つかった境界条件に対する新しいテストの追加を容易にします。

テストの中でいくつかの項目を順番にテストするのではなく、 データ駆動型のテストを使用することで、非常に似たコードの繰り返しを省くことができます。また、同じテストが各データサンプルに適用されるため、系統的で均一なテストが推奨されます。

テストがデータ駆動型である場合、テストのコマンドラインで、function:tag のようにテスト関数名とともに data タグを指定することで、その関数のすべてのテストケースではなく、特定の1つのテストケースに対してテストを実行することができます。これは、グローバルデータタグにも、関数自身のデータから行を特定するローカルタグにも使用できます。function:global:local のように、これらを組み合わせることもできます。

カバレッジ・ツールの使用

Cocogcovなどのカバレッジツールを使うと、テスト対象の関数やクラスで、できるだけ多くの文、分岐、条件をカバーするテストを書くことができます。これは、新機能の開発サイクルの早い段階で行えば行うほど、後でコードをリファクタリングしたときにリグレッションを発見しやすくなります。

テストを除外する適切なメカニズムの選択

適用できないテストを除外するために、適切なメカニズムを選択することが重要です。

QSKIP() を使って、実行時にテスト関数全体が現在のテスト環境では適用できないことが判明した場合に対処します。テスト関数の一部だけをスキップする場合は、条件文を使用することができます。オプションとして、qDebug() を呼び出して、適用できない部分をスキップする理由を報告することもできます。

最終的に修正されるべき既知のテスト失敗がある場合は、QEXPECT_FAIL を推奨する。また、Abort フラグを使用した場合でも、問題がまだ存在していることを確認し、コードのメンテナに知らせることができます。

データ駆動型テストのテスト関数やデータ行は、#if を使って、特定のプラットフォームや有効になっている特定の機能に限定することができます。ただし、#if を使用してテスト関数をスキップする場合は、moc の制限に注意してください。moc プリプロセッサは、コンパイラの機能検出によく使用される、コンパイラのすべてのbuiltin マクロにアクセスすることはできません。そのため、moc は、プリプロセッサの条件に対して、残りのコードで表示される結果とは異なる結果を得ることがあります。その結果、moc 、実際のコンパイラがスキップするテスト・スロットのメタデータが生成されたり、実際にクラスにコンパイルされるテスト・スロットのメタデータが省略されたりすることがあります。最初のケースでは、テストは実装されていないスロットを実行しようとします。2つ目のケースでは、テストスロットを実行する必要があるにもかかわらず、テストは実行しようとしません。

テストプログラム全体が特定のプラットフォームに適用できない場合や、特定の機能が有効になっていない場合は、親ディレクトリのビルド設定を使用してテストをビルドしないようにするのが最良の方法です。たとえば、tests/auto/gui/someclass のテストが macOS では無効である場合、tests/auto/gui/CMakeLists.txt のサブディレクトリとしてインクルードし、プラットフォームチェックを行います:

if(NOT APPLE)
    add_subdirectory(someclass)
endif

あるいは、qmake を使用する場合は、tests/auto/gui.pro に以下の行を追加する:

mac*: SUBDIRS -= someclass

QSKIPによるテストのスキップも参照のこと。

Q_ASSERTを避ける

Q_ASSERT マクロは、アサートされた条件がfalse になるたびにプログラムを中断させますが、これはソフトウェアがデバッグ・モードでビルドされた場合に限られます。リリースビルドでもデバッグ&リリースビルドでも、Q_ASSERT は何もしません。

Q_ASSERT デバッグビルドがテストされているかどうかによって、テストの動作が異なってしまうからです。また、テストが即座に中止され、残りのテスト関数がすべてスキップされ、不完全なテスト結果や不正なテスト結果が返されるからです。

また、テスト終了時に行われるはずだった撤収や整頓もスキップしてしまうため、ワークスペースが整頓されていない状 態になり、以降のテストが複雑になる可能性があります。

Q_ASSERT の代わりに、QCOMPARE() またはQVERIFY() マクロを使用する。これらのマクロを使用すると、現在のテストは失敗を報告して終了しますが、残りのテスト関数は実行でき、テストプログラム全体は正常に終了します。QVERIFY2() では、説明的なエラーメッセージをテストログに記録することもできます。

信頼できるテストの記述

以下のセクションでは、信頼性の高いテストを書くためのガイドラインを示します:

検証ステップにおける副作用の回避

QCOMPARE ()、QVERIFY ()などを使って自動テストで検証ステップを実行する場合、副作用は避けるべきである。検証ステップにおける副作用は、テストをわかりにくくします。また、QTRY_VERIFY ()、QTRY_COMPARE ()、QBENCHMARK () を使用するようにテストを変更すると、診断が難しい方法で簡単にテストを壊してしまう可能性があります。これらは渡された式を複数回実行するため、副作用が繰り返されます。

副作用が避けられない場合は、たとえテストが失敗しても、テスト関数の終了時に以前の状態に戻るようにします。これには一般的に、関数が戻ったときに状態を復元する RAII (resource acquisition is initialization) クラスか、cleanup() メソッドを使用する必要があります。単純にテストの最後に復元コードを記述してはいけません。テストの一部が失敗した場合、そのようなコードはスキップされ、以前の状態は復元されません。

固定タイムアウトは避ける

QTest::qWait() のような、ある条件が真になるのを待つための、ハードコードされたタイムアウトの使用は避けましょう。QSignalSpy クラス、QTRY_VERIFY ()またはQTRY_COMPARE ()マクロ、あるいはQSignalSpy クラスとQTRY_ マクロのバリアントを併用することを検討してください。

qWait() 関数を使用すると、あるアクションを実行してから、そのアクションによってトリガーされる非同期動作が完了するのを待つまでの間に、一定期間の遅延を設定することができます。例えば、ウィジェットの状態を変更してから、そのウィジェットが再描画されるのを待つような場合です。しかし、このようなタイムアウトは、ワークステーションで書かれたテストがデバイスで実行されたときに失敗することがよくあります。固定タイムアウトを、最も低速なテストプラットフォームで必要な値の数倍に増やすことは、良い解決策ではありません。

テスト対象のコードが非同期動作の完了時に Qt シグナルを発行する場合は、QSignalSpy クラスを使用して、検証ステップを実行できるようになったことをテスト関数に通知するのが良い方法です。

Qt シグナルがない場合は、QTRY_COMPARE()QTRY_VERIFY() マクロを使用します。 と マクロは、指定された条件が真になるか、ある最大タイムアウトに達するまで定期的にテストを行います。これらのマクロは、必要以上にテストに時間がかかるのを防ぐと同時に、ワークステーショ ンで書かれたテストが後で組み込みプラットフォームで実行されたときに壊れるのを防ぎます。

Qt シグナルがなく、新しい API の開発の一部としてテストを書いている場合は、非同期動作の完了を報告するシグナルを追加することで API にメリットがあるかどうかを検討してください。

タイミング依存の動作に注意

テスト戦略の中には、特定のクラスのタイミング依存の振る舞いに弱いものがあります。 これは、特定のプラットフォームでのみ失敗するテストや、一貫した結果を返さないテストにつながる可能性があります。

これは、特定のプラットフォームでのみ失敗するテストや、一貫した結果を返さないテストにつながる可能性があります。この例のひとつに、テキスト入力ウィジェットがあります。これは、テストを実行するマシンの速度に依存します。

タイマーイベントに基づいて状態を変化させるクラスをテストする場合、検証ステップを実行する際に、タイマーベースの動作を考慮に入れる必要があります。タイミングに依存する動作は多様であるため、このテスト問題に対する単一の汎用的な解決策はありません。

テキスト入力ウィジェットの場合、潜在的な解決策として、カーソルの点滅動作を無効にする(APIがその機能を提供している場合)、ビットマップをキャプチャする前にカーソルが既知の状態になるのを待つ(例えば、APIが適切なシグナルを提供している場合、そのシグナルにサブスクライブする)、またはビットマップ比較からカーソルを含む領域を除外する、などがあります。

ビットマップのキャプチャと比較を避ける

ビットマップのキャプチャと比較によるテスト結果の検証は、時には必要ですが、非常に壊れやすく、手間がかかります。

例えば、特定のウィジェットは、異なるプラットフォームや異なるウィジェットスタイルで異なる外観を持つ可能性があるため、参照ビットマップを複数回作成し、Qt がサポートするプラットフォームが進化するにつれて将来的に維持する必要があるかもしれません。したがって、ビットマップに影響を与えるような変更を加えるということは、サポートされる各プラットフォーム上で期待されるビットマップを再作成しなければならないということであり、各プラットフォームへのアクセスが必要になります。

ビットマップの比較は、テストマシンの画面解像度、ビット深度、アクティブなテーマ、配色、ウィジェットスタイル、アクティブなロケール(通貨記号、テキストの方向など)、フォントサイズ、透明効果、ウィンドウマネージャの選択などの要因によっても影響を受けます。

可能であれば、ビットマップをキャプチャして比較する代わりに、オブジェクトや変数のプロパティを検証するなど、プログラム的な手段を使用する。

テスト出力の改善

以下のセクションでは、読みやすく有用なテスト出力を作成するためのガイドラインを示します:

警告のテスト

ソフトウェアを構築するときと同じように、テスト出力が警告でごちゃごちゃしていると、 バグ発生の手がかりとなる警告に気づきにくくなります。ですから、定期的にテストログに警告やその他の余計な出力がないかチェックし、 その原因を調べるのが賢明です。それがバグの兆候である場合は、警告をテスト失敗の引き金にすることができます。

テスト対象のコードが、誤った使用法に関する警告のようなメッセージを出すべき場合は、 それが実際に使われたときに出るかどうかをテストすることも重要です。qWarning ()、qDebug ()、qInfo ()、QTest::ignoreMessage ()を使って、 ()、 ()、 ()、 ()、 ()、 ()、 ()。これにより、メッセージが生成されていることが確認され、テスト実行の出力からフィルタリングされます。メッセージが出力されない場合、テストは失敗します。

Qt がデバッグモードでビルドされたときにのみ期待されるメッセージが出力される場合は、QLibraryInfo::isDebugBuild ()を使用して、Qt ライブラリがデバッグモードでビルドされたかどうかを判断してください。#ifdef QT_DEBUG を使うだけでは十分ではありません。テストがデバッグモードでビルドされたかどうかがわかるだけで、Qt ライブラリもデバッグモードでビルドされたかどうかは保証されません。

Qt 6.3 以降のテストでは、QTest::failOnWarning() を呼び出すことで、qWarning() への呼び出しがトリガされないことを確認できます。これは、テストする警告メッセージか、警告とマッチするQRegularExpression を取ります。マッチする警告が生成されると、それが報告され、テストが失敗する原因となります。たとえば、警告をまったく生成しないはずのテストは、QTest::failOnWarning(QRegularExpression(u".*"_s)) 、警告にまったくマッチしないことになります。

環境変数QT_FATAL_WARNINGS を設定することで、警告を致命的なエラーとして扱うこともできます。詳細はqWarning() を参照してください。これは自動テストに限ったことではありません。そうしないと膨大なテストログから警告が消えてしまう場合、この環境変数を設定して時折実行することで、発生した警告を見つけて取り除くことができます。

自動テストからのデバッグメッセージの出力を避ける

オートテストは、未処理の警告メッセージやデバッグメッセージを出力すべきではありません。これにより、CI Gate は新しい警告メッセージやデバッグメッセージをテストの失敗として扱うことができます。

開発中にデバッグメッセージを追加するのは構いませんが、これらはテストがチェックインされる前に無効にするか削除してください。

構造化された診断コードを書く

テストが失敗した場合に有用な診断出力は、コメントアウトしたりプリプロセッサディレクティブで無効にしたりデバッグビルドでのみ有効にしたりするのではなく、通常のテスト出力の一部とすべきです。継続的インテグレーション中にテストが失敗した場合、CI ログに関連するすべての診断出力があれば、診断コードを有効にしてテストをやり直すよりも多くの時間を節約できます。特に、デスクトップにないプラットフォームで失敗した場合はそうです。

テストの診断メッセージは、stdio.hiostream.h のような出力メカニズムではなく、qDebug()qWarning() のような Qt の出力メカニズムを使うべきです。後者は Qt のメッセージ処理をバイパスし、-silent コマンドラインオプションが診断メッセージを抑制するのを防ぎます。その結果、重要な失敗メッセージが大量のデバッグ出力に隠れてしまう可能性があります。

テスト可能なコードを書く

以下のセクションでは、テストしやすいコードを書くためのガイドラインを示します:

依存関係の解消

ユニットテストの考え方は、すべてのクラスを分離して使うことです。多くのクラスは他のクラスをインスタンス化するので、ひとつのクラスを個別にインスタンス化することはできません。そのため、依存性の注入と呼ばれるテクニックを使って、オブジェクトの作成とオブジェクトの使用を分離する必要があります。ファクトリーはオブジェクトツリーの構築を担当します。他のオブジェクトは、抽象インターフェースを介してこれらのオブジェクトを操作します。

このテクニックは、データ駆動型のアプリケーションに有効です。GUIアプリケーションの場合、オブジェクトの生成と破棄が頻繁に行われるため、このアプローチは難しいかもしれません。抽象インターフェースに依存するクラスの正しい動作を検証するために、モッキングを使用することができる。例えば、Googletest Mocking (gMock) Frameworkを参照してください。

すべてのクラスをライブラリにコンパイルする

小規模から中規模のプロジェクトでは、ビルド・スクリプトは通常すべてのソース・ファイルをリストアップし、実行ファイルを一度にコンパイルします。つまり、テスト用のビルド・スクリプトは、必要なソース・ファイルを再度リストアップしなければなりません。

スタティック・ライブラリーをビルドするスクリプトでは、ソース・ファイルとヘッダーを一度だけリストする方が簡単です。その後、main() 関数をスタティック・ライブラリにリンクして実行ファイルをビルドし、テストをスタティック・ライブラリにリンクします。

同じソース・ファイルを複数のプログラムのビルドに使用するプロジェクトでは、共有クラスを動的にリンクされた(または共有オブジェクト)ライブラリにビルドし、テスト・プログラムを含む各プログラムが実行時にロードできるようにする方が適切な場合があります。この場合も、コンパイルされたコードをライブラリに格納することで、どのコンポーネントを組み合わせて様々なプログラムを作成するかという記述の重複を避けることができます。

テストマシンのセットアップ

以下のセクションでは、テスト機のセットアップに起因する一般的な問題について説明します:

これらの問題はすべて、仮想化をうまく利用することで解決できます。

スクリーンセーバー

スクリーンセーバーは、GUI クラスのテストの一部を妨害し、信頼できないテスト結果を引き起こす可能性があります。テスト結果の一貫性と信頼性を確保するため、スクリーンセーバーは無効にしてください。

システムダイアログ

オペレーティングシステムや他の実行中のアプリケーションによって不意に表示されるダイアログは、自動テストに関与するウィジェットから入力フォーカスを奪い、再現性のない失敗を引き起こす可能性があります。

典型的な問題の例としては、macOSのオンラインアップデート通知ダイアログ、ウイルススキャナの誤報、ウイルスシグネチャのアップデートなどのスケジュールタスク、ワークステーションにプッシュされるソフトウェアアップデート、スタックの上にウィンドウをポップアップするチャットプログラムなどがあります。

ディスプレイの使用

一部のテストでは、テストマシンのディスプレイ、マウス、キーボードを使用するため、マシンが同時に他のことに使用されていたり、複数のテストが並行して実行されていたりすると失敗する可能性がある。

CIシステムはこの問題を避けるために専用のテストマシンを使用しているが、専用のテストマシンがない場合は、セカンドディスプレイ上でテストを実行することでこの問題を解決できるかもしれない。

Unix では、Xephyr のようなネストされた X サーバーや仮想 X サーバー上でテストを実行することもできる。例えば、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-serverで異なるlibGLバージョンでテストを実行すると、QMLディスクキャッシュによってテストがクラッシュすることがあります。これを避けるには、QML_DISABLE_DISK_CACHE=1.

または、offscreenプラグインを使う:

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

ウィンドウマネージャ

Unixでは、少なくとも2つの自動テスト(tst_examplestst_gestures )でウィンドウマネージャの実行が必要です。したがって、これらのテストをネストした X サーバーで実行する場合は、その X サーバーでもウィンドウマネージャを実行する必要があります。

ウィンドウ・マネージャは、すべてのウィンドウをディスプレイ上に自動的に配置するように設定する必要があります。タブウィンドウマネージャ(twm)のようないくつかのウィンドウマネージャには、新しいウィンドウを手動で配置するモードがあります。

注: タブウィンドウマネージャは、tst_gestures の自動テストでは、設定を忘れて手動でのウィンドウ配置に戻ってしまうため、Qt の自動テスト一式を実行するのには適していません。

2024 The Qt CompanyLtd. 本書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。