#include Test Double

出典:
http://www.renaissancesoftware.net/blog/archives/231
Copyright © James Grenning
著者のJames Grenning氏の許諾を得たので日本語訳を公開します。

今日はレガシーなCコードにテストを追加する1日目だ。しかし、テストハーネスに入れようとしているコードが開発マシン上ではコンパイルできずに、にっちもさっちも行かなくなってしまった。クラッシュさせて成功させる(Crash to Pass)ための「コンパイルを成功させる」ステップで行き詰ってしまったということだ。

レガシーな組み込みCコード(テストのない組み込みCコード)をテストハーネスに入れるのは骨の折れる作業になる。レガシーCコードは、ターゲットのプロセッサに強く結びついていることが多い。プロダクトコードでは問題にならないかもしれないが、オフターゲットのユニットテストでは大きな問題となる。

C言語には、依存関係を断ち切るためのメカニズムがあまりない。私の本では、関数ポインタによる置き換えについて多くのページを割いて説明したが、プリプロセッサによるスタブ化についてはほんの触り程度だった。

この記事では、問題のある#includeファイルの依存関係を断ち切る方法として、#include Test-Doubleについて見ていこう。

マイコンベンダーから提供された、ターゲット依存のヘッダーファイルがあるとしよう。これは私が先週出くわしたのと同じようなものだ:

/*
 *  ---- acmetypes.h ----
 */
 
#ifndef _ACME_STD_TYPES
#define _ACME_STD_TYPES
 
#if defined(_ACME_X42)
    typedef unsigned int        Uint_32;
    typedef unsigned short      Uint_16;
    typedef unsigned char       Uint_8;
 
    typedef int                 Int_32;
    typedef short               Int_16;
    typedef char                Int_8;
 
#elif defined(_ACME_A12)
    typedef unsigned long       Uint_32;
    typedef unsigned int        Uint_16;
    typedef unsigned char       Uint_8;
 
    typedef long                Int_32;
    typedef int                 Int_16;
    typedef char                Int_8;
#else
    #error <acmetypes.h> is not supported for this environment
#endif
 
#endif  /* _ACME_STD_TYPES */

Cのプリプロセッサは#errorに到達した時点で停止してしまう。単に_ACME_X42か_ACME_A12をdefineすればよいと思うかもしれない。しかし、オフターゲットでのテストを行っている場合、とくに(私がこの文章を書くのに使っているのと同じように)64ビットマシンを使っている場合はint型のサイズが問題となる。

どんなときでもプロダクトコードのヘッダーファイルをインクルードできれば一番だが、上手くいかないときもある。とりわけレガシーコードを相手にしているときはそうだ。だが、あきらめる必要はない。そういう時は、#include Test-Doubleを使おう。

今回のケースはわりと簡単だ。同じ名前で新たなヘッダーファイルを作成し、プロダクトコードのヘッダーファイルよりも先に検索されるインクルードパス上に保存しよう。#include Test-Doubleは以下のようになる:

/*
 * acmetypes.h - test double for off target testing
 */
 
#ifndef ACMETYPES_H_
#define ACMETYPES_H_
 
#include <stdint.h>
 
typedef uint32_t		Uint_32;
typedef uint16_t		Uint_16;
typedef uint8_t			Uint_8;
 
typedef int32_t			Int_32;
typedef int16_t			Int_16;
typedef int8_t			Int_8;
 
#endif /* ACMETYPES_H_ */

オフターゲットのホスト開発システム環境ではネイティブ実装のstdint.hがあるだろう。それが固有のintサイズに対処しなければならない場合の一般的な方法だ。上の例では、単純にAcmeの型を可搬性のある型で再定義している。

CppUTestとmakefileを使っているなら、以下のようにしてプロダクトコードのヘッダーファイルより先にテストダブルのヘッダーファイルを探しに行くようにmakefileに指示する事ができる:

INCLUDE_DIRS =\
  .\
  include \
  include/* \
  $(CPPUTEST_HOME)/include/ \
  mocks/includes \
  $(ACME_INCLUDES) \

$(ACME_INCLUDES)はマイコンベンダー依存のインクルードファイルが置かれたディレクトリを指す。テストダブル用のヘッダーファイルをmocks/includesなどのフォルダに置こう。INCLUDE_DIRS上でmocks/includesが$(ACME_INCLUDES)より前にあれば、テストダブル用のヘッダーファイルでプロダクトコードのヘッダーファイルを置き換えることができる。プロダクトコードをビルドするときは、mocks/includesはインクルードパスには含まれない。

CppUTestのmakefileを使わなくても同じことができる。私が知っている全てのコンパイラは、インクルードパスをサポートしている。

全てが上手く行っている事を確認するために、次のようなテストケースを書くことができる。

extern "C"
{
#include "acmetypes.h"
}
 
#include "CppUTest/TestHarness.h"
 
TEST_GROUP(acmetypes) {};
 
TEST(acmetypes, checkIntSizes)
{
	LONGS_EQUAL(1, sizeof(Uint_8));
	LONGS_EQUAL(1, sizeof(Int_8));
	LONGS_EQUAL(2, sizeof(Uint_16));
	LONGS_EQUAL(2, sizeof(Int_16));
	LONGS_EQUAL(4, sizeof(Uint_32));
	LONGS_EQUAL(4, sizeof(Int_32));
}

ほかにも#includeスタブを使える状況がある。例えば、数個のシンボルだけが必要なときに、長いインクルードのチェインを断ち切りたい場合だ。だが、プロダクトコードのヘッダーが使えるならそれが一番だということは心に留めておいて欲しい。

この記事が役にたったなら教えて欲しい。もし必要な情報が足りない場合も教えて欲しい。