純粋なCでの単体テストの半自動登録

書籍Embedded Embedded Cのテスト駆動開発を読んだ後、cppUtestフレームワークを使用したユニットテストの世界について紹介し始めました。 特に、新しく書かれたテストが登録され、独立して起動されるためです。 あなたはそれを支払う必要があります-C ++を使用して、フレームワークの深さのどこかに動的なメモリ割り当て。 たぶんそれはもっと簡単にできますか?

最近では、わずか4行に収まるミニマルなminUnitフレームワークについて学びました。



明確にするためにここで説明します。



#define mu_assert(message, test) do { if (!(test)) return message; } while (0) #define mu_run_test(test) do { char *message = test(); tests_run++; \ if (message) return message; } while (0) extern int tests_run;
      
      





シンプルで美しい。 同時に、テストの記述は次のようになります。



 static char * test_foo() { mu_assert("error, foo != 7", foo == 7); return 0; }
      
      





残念ながら、このフレームワークを使用しようとすると、各テストを手で登録するのが非常に面倒であることにすぐに気付きました。 結局のところ、テストでファイルのヘッダーファイルを開始し、このファイルの各テストに宣言を記述してから、メインに移動して呼び出しを登録する必要があります!



純粋なCで書かれた他のフレームワークを見ました:ほとんどどこでも同じこと。 別の方法として、テストでソースコードをスキャンし、実行するコードを生成する個別のプログラムが提供されます。

しかし、おそらくもっと簡単にできるのでしょうか?



この投稿は、リンカーを使用してテストを登録するという自信を植え付けました。 しかし、リンカおよびコンパイラ固有の属性にアタッチしたくありませんでした。

私の知る限り、純粋なCでは完全なテスト登録を行うことは不可能です。 半自動はどうですか?



このアイデアは次のように形になりました。 モジュールごとに、module_tests.cファイルが書き込まれ、このモジュールのすべてのテストが書き込まれます。 これらのテストはグループを形成します。 同じファイルに、グループ内のすべてのテストを実行するマジック機能が記述されています。

そして主に、各テストを個別にではなく、グループの立ち上げを手で登録するだけです。

これは、次のタスクに要約されます。ファイル内のすべての関数のリストを何らかの方法で取得する必要があります。 Cでは、これはプリプロセッサを使用してのみ実行できます。 しかし、どのように? たとえば、関数が何らかの形で単調に呼び出される場合。



テストのタイトルのみがわかりやすい場合、テストの「サービス」名は何でもかまいません。

これは、プリプロセッサの助けを借りて、さらにテスト関数の名前を、単一のテンプレートに基づいて均一に生成する必要があることを意味します。 たとえば、次のように:



 #define UMBA_TEST_COUNTER BOOST_PP_COUNTER #define UMBA_TEST_INCREMENT() BOOST_PP_UPDATE_COUNTER() #define UMBA_TOKEN(x, y, z) x ## y ## z #define UMBA_TOKEN2(x, y, z) UMBA_TOKEN(x,y,z) #define UMBA_TEST( description ) static char * UMBA_TOKEN2(umba_test_, UMBA_TEST_COUNTER, _(void) )
      
      





率直に言って、私は人生で初めてブーストを使用しましたが、Cプリプロセッサの力に驚かされました!

これで、次のようにテストを作成できます。



 UMBA_TEST("Simple Test") //  static char * umba_test_0_(void) { uint8_t a = 1; uint8_t b = 2; UMBA_CHECK(a == b, "MATHS BROKE"); return 0; } #include UMBA_TEST_INCREMENT()
      
      





このインクルードの後、カウンターがインクリメントされ、次のテストの名前が生成されます。名前は静的char * umba_test_1_(void)です。



ファイル内のすべてのテストを実行する関数を生成するためだけに残ります。 これを行うために、関数へのポインターの配列が作成され、テストへのポインターが取り込まれます。 次に、関数はループ内の配列から各テストを呼び出すだけです。

この関数は、UMBA_TEST_COUNTERの値が最後のテストの数と等しくなるように、テストファイルの最後に記述する必要があります。

ポインターの配列を生成するために、最初に単純なパスに従って、次のようなヘルパーファイルを作成しました。



 #if UMBA_TEST_COUNTER == 1 #define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = {umba_test_0_}; #elif UMBA_TEST_COUNTER == 2 #define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = {umba_test_0_, umba_test_1_}; …
      
      





原則として、数百のテスト用の広告を生成することにより、これでうまくいくことができます。 その後、boost'aから必要なファイルは、boost / preprocessor / slot / counter.hppの1つだけです。

しかし、ブーストを使い始めたので、続けてみませんか?



 #define UMBA_DECL(z, n, text) text ## n ## _, #define UMBA_LOCAL_TEST_ARRAY UmbaTest umba_local_test_array[ UMBA_TEST_COUNTER ] = { BOOST_PP_REPEAT( UMBA_TEST_COUNTER, UMBA_DECL, umba_test_ ) }
      
      





2行だけですが、その背後にはどのような力が隠されています!

グループ起動機能自体の簡単なコードを追加します。



 #define UMBA_RUN_LOCAL_TEST_GROUP( groupName ) UMBA_LOCAL_TEST_ARRAY; \ char * umba_run_test_group_ ## groupName ## _(void) \ { \ for(uint32_t i=0; i < UMBA_TEST_COUNTER; i++) \ { \ tests_run++; \ char * message = umba_local_test_array[i](); \ if(message) \ return message; \ } \ return 0; \ } \
      
      





そして、メインから開始するには:



 #define UMBA_EXTERN_TEST_GROUP( groupName ) char * umba_run_test_group_ ## groupName ## _(void); #define UMBA_RUN_GROUP( groupName ) do { \ char *message = umba_run_test_group_ ## groupName ## _(); \ tests_run++; \ if (message) return message; \ } while (0)
      
      





出来上がり。 これで、任意の数のテストでグループを開始するのは同じように見えます。



 UMBA_EXTERN_TEST_GROUP( SimpleGroup ) static char * run_all_tests(void) { UMBA_RUN_GROUP( SimpleGroup ); return 0; } int main(void) { char *result = run_all_tests(); if (result != 0 ) { printf("!!!!!!!!!!!!!!!!!!!\n"); printf("%s\n", result); } else { printf("ALL TESTS PASSED\n"); } printf("Tests run: %d\n", tests_run-1); return 0; }
      
      







この結果には非常に満足しています。 テストを作成するときの機械的動作が大幅に少なくなりました。

上記のすべてのマクロは40〜50行に収まりますが、残念なことに、これはminUnitよりもやや大きくなります(そしてあまり明白ではありません)。

コード全体。



はい、大規模なフレームワークの機能はそれほど多くありませんが、正直なところ、CHECKなどの単純なチェック(trueの場合)以外のテストで使用する時間がありませんでした。

テストの説明は単純に破棄されますが、それを使用して何か便利なことをするのは、突然したい場合は簡単に思えます。



私が知りたいこと:

  1. 新しい何かを発明したか、長年このトリックを使用していますか?
  2. これは何とか改善できますか? 各テストの後に奇妙なインクルードを記述する必要はあまりありませんが、プリプロセッサに他のカウンタ実装が見つかりませんでした(私のコンパイラは__COUNT__をサポートしていません)。
  3. 本番環境で間に合わせのフレームワークを使用する必要がありますか?
  4. BOOST_PP_COUNTERの仕組みは? スタックオーバーフローでも、対応する質問への答えは「魔法」です。



All Articles