最近、 zerocostは興味深い記事「マクロと動的メモリを使用しないC ++でのテスト」を執筆し、C ++コードをテストするための最小限のフレームワークについて説明しています。 著者は(ほとんど)テストを登録するためにマクロを使用することを避けましたが、それらの代わりに、「魔法の」テンプレートがコードに現れました。 記事を読んだ後、私は何がより良いことができるかを知っていたので、不満の漠然とした感覚がありました。 私はすぐにどこを思い出すことができませんでしたが、私は間違いなくそれらを登録するための単一の余分な文字が含まれていないテストコードを見ました:
void test_object_addition() { ensure_equals("2 + 2 = ?", 2 + 2, 4); }
最後に、このフレームワークはCutterと呼ばれ、天才的な方法を使用して独自の方法でテスト関数を識別することを思い出しました。
(CC BY-SAでCutterウェブサイトから取得したKDPV。)
トリックは何ですか?
テストコードは、個別の共有ライブラリにコンパイルされます。 テスト関数は、エクスポートされたライブラリシンボルから抽出され、名前で識別されます。 テストは、特別な外部ユーティリティによって実行されます。 サピエンティ
$ cat test_addition.c #include <cutter.h> void test_addition() { cut_assert_equal_int(2 + 2, 5); }
$ cc -shared -o test_addition.so \ -I/usr/include/cutter -lcutter \ test_addition.c
$ cutter . F ========================================================================= Failure: test_addition <2 + 2 == 5> expected: <4> actual: <5> test_addition.c:5: void test_addition(): cut_assert_equal_int(2 + 2, 5, ) ========================================================================= Finished in 0.000943 seconds (total: 0.000615 seconds) 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s) 0% passed
Cutterドキュメンテーションの例を次に示します 。 Autotoolsに関連するすべてを安全にスクロールして、コードのみを見ることができます。 フレームワークは少し奇妙です、はい、すべての日本のように。
実装機能についてはあまり詳しく説明しません。 また、個人的には実際には必要ないので、本格的な(少なくともドラフトでも)コードはありません(Rustではすべてがすぐに使用できます)。 ただし、興味のある人にとっては、これは良い練習になります。
詳細と実装オプション
Cutterアプローチを使用してテスト用のフレームワークを作成するときに解決する必要があるタスクのいくつかを検討してください。
エクスポートされた関数の取得
まず、何らかの方法でテスト機能にアクセスする必要があります。 もちろん、C ++標準では、共有ライブラリはまったく記述されていません。 Windowsは最近、Linuxサブシステムを買収しました。これにより、3つの主要なオペレーティングシステムすべてをPOSIXに縮小できます。 ご存じのように、POSIXシステムは関数dlopen()
、 dlsym()
、 dlclose()
を提供します。これらを使用して、関数のアドレスを取得し、そのシンボルの名前を知ることができます。 ロードされたライブラリに含まれる関数のリストは、POSIXによって公開されていません。
残念ながら(かなり幸いですが)、ライブラリからエクスポートされたすべての関数を見つけるための標準的な移植可能な方法はありません。 おそらく、 ライブラリの概念がすべてのプラットフォーム上に存在するわけではないという事実(参照:組み込み)がここで何らかの形で関係しています。 しかし、それはポイントではありません。 主なことは、プラットフォーム固有の機能を使用する必要があることです。
最初の近似として、単にnmユーティリティを呼び出すことができます。
$ cat test.cpp void test_object_addition() { }
$ clang -shared test.cpp
$ nm -gj ./a.out __Z20test_object_additionv dyld_stub_binder
出力を解析し、 dlsym()
を使用します。
内観を深めるには 、 libelf 、 libMachO 、 pe-parseなどのライブラリが便利です。これにより、実行可能なファイルや関心のあるプラットフォームのライブラリをプログラムで解析できます。 実際、 nmと会社はそれらを使用するだけです。
テスト関数のフィルタリング
お気づきかもしれませんが、ライブラリには奇妙な文字が含まれています。
__Z20test_object_additionv dyld_stub_binder
これが__Z20test_object_additionv
は何か、関数test_object_addition
を呼び出したときですか? そして、この左のdyld_stub_binder
は何ですか?
「 __Z20...
」文字__Z20...
は、いわゆる名前装飾 (名前マングリング)です。 C ++コンパイル機能。何もできません。 これが、システムの観点から(およびdlsym()
)関数が呼び出されるものです。 それらを通常の形で人に見せるために、 libdemangleのようなライブラリを使用できます。 もちろん、必要なライブラリは使用するコンパイラによって異なりますが、装飾形式は通常、プラットフォームのフレームワーク内で同じです。
dyld_stub_binder
ような奇妙な関数dyld_stub_binder
、これらも考慮に入れなければならないプラットフォーム機能です。 魚がいないので、テストを開始するときに関数を呼び出す必要はありません。
この考えの論理的な継続は、関数を名前でフィルタリングすることです。 たとえば、名前にtest
れる関数のみを実行できます。 または、単にtests
名前空間から機能します。 また、ネストされた名前空間を使用してテストをグループ化します。 あなたの想像力に制限はありません。
実行可能テストのコンテキストを渡す
テスト付きのオブジェクトファイルは共有ライブラリに収集され、そのコードの実行は外部ユーティリティドライバー(Cutter用のカッター)によって完全に制御されます。 したがって、内部テスト機能はこれを使用できます。
たとえば、実行可能テスト(元の記事のIRuntime
)のコンテキストは、グローバル(スレッドローカル)変数を介して安全に渡すことができます。 ドライバーは、コンテキストの管理と受け渡しを担当します。
この場合、テスト関数は引数を必要としませんが、テストされたケースの任意の命名など、すべての高度な機能を保持します。
void test_vector_add_element() { testing::description("vector size grows after push_back()"); }
description()
関数は、グローバル変数を介して条件付きIRuntime
アクセスするため、人のフレームワークにコメントを渡すことができます。 グローバルコンテキストを使用するセキュリティはフレームワークによって保証されており、テスト作成者の責任ではありません。
このアプローチでは、コンテキストを比較ステートメントおよびメインテスト関数から呼び出す必要がある内部テスト関数に転送することで、コードのノイズが少なくなります。
コンストラクターとデストラクター
テストの実行はドライバーによって完全に制御されるため、テストの周りで追加のコードを実行できます。
Cutterライブラリは、このために次の関数を使用します。
cut_setup()
-個々のテストの前cut_teardown()
-個々のテストの後cut_startup()
-すべてのテストを実行する前にcut_shutdown()
-すべてのテストの完了後
これらの関数は、テストファイルで定義されている場合にのみ呼び出されます。 テスト環境(フィクスチャ)の準備とクリーニングを行うことができます:必要な一時ファイルの作成、テストされたオブジェクトの複雑なセットアップ、およびテストの他のアンチパターン。
C ++の場合、より慣用的なインターフェイスを考え出すことができます。
- よりオブジェクト指向でタイプセーフ
- より良いRAIIコンセプトのサポート
- 遅延実行にラムダを使用する
- テスト実行コンテキストを含む
しかし、今のところ、私は今、このことについてもう一度詳細に考えています。
自己完結型のテスト実行可能ファイル
Cutterは便宜上、共有ライブラリアプローチを使用しています。 さまざまなテストが一連のライブラリにコンパイルされ、個別のテストユーティリティが検出して実行します。 もちろん、必要に応じて、テストドライバーのコード全体を実行可能ファイルに直接埋め込み、通常の個別のファイルを取得できます。 ただし、これらの実行可能ファイルのレイアウトを正しい方法で整理するには、ビルドシステムとのコラボレーションが必要になります。「未使用」機能を削除せずに、正しい依存関係を使用するなどです。
その他
Cutterやその他のフレームワークには、テストを作成するときに作業を楽にする他の多くの便利な機能もあります。
- 柔軟で拡張可能なテストステートメント
- ファイルからのテストデータの構築と取得
- スタックトレース調査、例外およびドロップ処理
- テストのカスタマイズ可能な「ブレークダウンレベル」
- 複数のプロセスでテストを実行する
自転車を書くときは、既存のフレームワークを振り返ってみる価値があります。 UXははるかに深いトピックです。
おわりに
Cutterフレームワークで使用されるアプローチにより、プログラマーの認知的負荷を最小限に抑えてテスト関数を特定できます。テスト関数を記述するだけです。 コードでは、特別なテンプレートやマクロを使用する必要がないため、読みやすくなります。
テストのアセンブルと実行の機能は、Makefile、CMakeなどのアセンブリシステム用の再利用可能なモジュールに隠すことができます。テストの別のアセンブリに関する質問は、何らかの形で尋ねる必要があります。
このアプローチの欠点は、テストをメインコードと同じファイル(同じ翻訳単位)に配置することが難しいことです。 残念ながら、この場合、追加のヒントがなければ、どの機能を起動する必要があるか、どの機能を起動する必要がないかを判断することはできません。 幸いなことに、C ++では、テストと実装を異なるファイルに配布するのが通常です。
マクロの最終的な処分に関しては、 原則としてそれらを放棄すべきではないようです。 マクロを使用すると、たとえば、コードの重複を避けて、短い比較ステートメントを記述できます。
void test_object_addition() { ensure_equals(2 + 2, 5); }
ただし、エラーが発生した場合、問題の情報内容は同じままにします。
Failure: test_object_addition <ensure_equals(2 + 2, 5)> expected: <5> actual: <4> test.c:5: test_object_addition()
理論的には、テストされる関数の名前、ファイル名、および関数の先頭の行番号は、収集されるライブラリに含まれるデバッグ情報から抽出できます。 比較式の期待値と実際の値は、 ensure_equals()
関数でensure_equals()
れます。 このマクロを使用すると、テストステートメントの元のスペルを「復元」できます。これにより、値4
が正確に期待される理由がより明確になります。
しかし、これは万人向けではありません。 テストコードのマクロの利点はこれで終わりですか? この瞬間についてはまだ考えていませんが、さらに良い分野になるかもしれません 倒錯 研究。 さらに興味深い質問:マクロなしでC ++のモックフレームワークを何らかの形で作成することは可能ですか?
気配りのある読者は、実装にSMSとアスベストが実際に存在しないことにも注意しました。これは、地球の生態系と経済にとって間違いなくプラスです。