Accessing static Data and Functions in Legacy C — Part 1

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

うるう年バグおめでとう(And a Happy Leap Year Bug)

新たな年がやってきた。昨年はうるう年だったため、4年に1度のうるう年バグレポートが上がってきた。Apple、TomTom、そしてMicrosoftからの謝罪文がメディアで取り上げられている。中国では列車の運行が止まったようだ。不具合(glitch)といってしまうと、なんだか他人の失敗で自分ではどうしようもできないように聞こえてしまう。うるう年というのはいつから存在するのだろうか?ジュリアス・シーザーがうるう年を導入したのは2000年以上前のローマ帝国時代になる。グレゴリオ暦は1582年から始まった。つまりこれは新しいアイディアでも新しいバグでもない。

このようなバグを作りこむプログラマたちの言い訳の1つに対して、いつも聞かれる質問に答えることで反論してみようと思う。その質問とは「どうすればCコード中のstatic関数をテストできるの?」というものだ。

TDDで書かれたコードであれば、static関数は公開インターフェースを介して間接的にテストされている。この記事で書いたように、TDDはコード不腐敗レーダーの役割を果たし、設計に問題があると気づかせてくれる。Cコードで隠蔽された関数とデータへの直接アクセスが必要ならば、それはコード腐敗のサインだ。そんなときはリファクタリングをしよう。

だが、staticな関数やデータを持ったレガシーコードはどうすればよいだろう?おそらく理想主義が通用する時期はとうに過ぎているため、なにか現実主義的な方法を適用すべきだろう。この記事と次の記事では、手付かずのコードをテストハーネスに入れて、やっかいなstatic変数と関数にアクセスする方法について見ていく。

もしコードを触ることに抵抗がないのであれば、すべてのstaticをSTATICに変更する手が使える。次に、プリプロセッサを使って、プロダクトコードのビルド時のみSTATICをstaticに置換することで、グローバルアクセスを得ることができる。gccでは以下のコマンドラインオプションが使える

  • プロダクトビルド -dSTATIC=static
  • テストビルド -dSTATIC

ここからは、コードに触らずにstaticにアクセスするための2つの方法を見ていく。1つ目は、テストファイルに.cファイルをインクルードする方法だ。次の記事では、隠蔽された部分へのアクセスを提供するためのテスト用アダプタを作成する。

ここで、Zune Bugの元になったオリジナルのコードによく似たコードを再度見てみよう。弁護士がやってくるのは嫌なので書き換えはしたが、このコードはオリジナルのZuneドライバと同じような構造をしており、staticなデータと関数を持っている。

ヘッダファイルは、日付に関する情報を得るためのデータ構造と初期化関数を提供する。

typedef struct Date
{
    int daysSince1980;
    int year;
    int dayOfYear;
    int month;
    int dayOfMonth;
    int dayOfWeek;
} Date;
 
void Date_Init(Date *, int daysSince1980);
 
enum {
    SUN = 0, MON, TUE, WED, THU, FRI, SAT
};
 
enum {
    JAN = 0, FEB, MAR, APR, MAY, JUN,
    JUL, AUG, SEP, OCT, NOV, DEC
};

Date_Initは、引数で渡されたDateインスタンスに値をセットする。daySince1980を操作してDate構造体の結果を検証すれば、この機能はフルにテストできる。しかし、その事実はあえて無視して、どうすれば隠蔽された関数とデータを直接テストできるかを見ていこう。

Date_Initは3つの隠蔽されたヘルパー関数を持つ。

void Date_Init(Date* date, int daysSince1980)
{
     date->daysSince1980 = daysSince1980;
     FirstSetYearAndDayOfYear(date);
     ThenSetMonthAndDayOfMonth(date);
     FinallySetDayOfWeek(date);
}

Date_Initは氷山の一角に過ぎない。興味があるのは隠蔽されたデータと関数の方だ。

#include "Date.h"
#include <stdbool.h>
 
enum
{
    STARTING_YEAR = 1980, STARTING_WEEKDAY = TUE
};
 
static const int nonLeapYearDaysPerMonth[12] =
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
 
static const int leapYearDaysPerMonth[12] =
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
 
static bool IsLeapYear(int year)
{
    if (year % 400 == 0)
        return true;
    if (year % 100 == 0)
        return false;
    if (year % 4 == 0)
        return true;
    return false;
}
 
static int GetDaysInYear(int year)
{
    if (IsLeapYear(year))
        return 366;
    else
        return 365;
}
 
static void FirstSetYearAndDayOfYear(Date * date)
{
    int days = date->daysSince1980;
    int year = STARTING_YEAR;
    int daysInYear = GetDaysInYear(year);
 
    while (days > daysInYear)
    {
        year++;
        days -= daysInYear;
        daysInYear = GetDaysInYear(year);
    }
 
    date->dayOfYear = days;
    date->year = year;
}
 
static void ThenSetMonthAndDayOfMonth(Date * date)
{
    int month = 0;
    int days = date->dayOfYear;
    const int * daysPerMonth = nonLeapYearDaysPerMonth;
    if (IsLeapYear(date->year))
        daysPerMonth = leapYearDaysPerMonth;
 
    while (days > daysPerMonth[month])
    {
        days -= daysPerMonth[month];
        month++;
    }
    date->month = month + 1;
    date->dayOfMonth = days;
}
 
static void FinallySetDayOfWeek(Date * date)
{
     date->dayOfWeek =  ((date->daysSince1980-1)+STARTING_WEEKDAY)%7;
}
 
void Date_Init(Date* date, int daysSince1980)
{
     date->daysSince1980 = daysSince1980;
     FirstSetYearAndDayOfYear(date);
     ThenSetMonthAndDayOfMonth(date);
     FinallySetDayOfWeek(date);
}

DaysPerMonth配列をチェックしたいとしよう。私たちがアメリカで使っている次の便利な詩にしたがってテストを書いてもよい。「30日あるのは 9月と4月と6月と11月 のこりの月は みんな31日 2月だけはとくべつ ふつうは28日で うるうどしは29日」

このテストから書き始めたとすると・・・

TEST(Date, sept_has_30_days)
{
    LONGS_EQUAL(30, nonLeapYearDaysPerMonth[SEP]);
}

・・・次のエラーが出る。

DateTest.cpp:41: error: 'nonLeapYearDaysPerMonth' was not declared in this scope

Date.hのかわりにDate.cをインクルードすることで隠蔽されたstaticへのアクセスを得ることにしよう。テストファイル全体は以下のようになる。

#include "CppUTest/TestHarness.h"
 
extern "C"
{
#include "Date.c"
}
 
TEST_GROUP(Date)
{
};
 
TEST(Date, sept_has_30_days)
{
    LONGS_EQUAL(30, nonLeapYearDaysPerMonth[SEP]);
}

すこしリファクタリングすると、DaysPerMonth配列は次のようにチェックできる。

#include "CppUTest/TestHarness.h"
 
extern "C"
{
#include "Date.c"
}
 
TEST_GROUP(Date)
{
    const int * daysPerYearVector;
 
    void setup()
    {
        daysPerYearVector = nonLeapYearDaysPerMonth;
    }
 
    void itsLeapYear()
    {
        daysPerYearVector = leapYearDaysPerMonth;
    }
 
    void CheckNumberOfDaysPerMonth(int month, int days)
    {
        LONGS_EQUAL(days, daysPerYearVector[month]);
    }
 
    void ThirtyDaysHasSeptEtc()
    {
        CheckNumberOfDaysPerMonth(SEP, 30);
        CheckNumberOfDaysPerMonth(APR, 30);
        CheckNumberOfDaysPerMonth(JUN, 30);
        CheckNumberOfDaysPerMonth(NOV, 30);
 
        CheckNumberOfDaysPerMonth(OCT, 31);
        CheckNumberOfDaysPerMonth(DEC, 31);
        CheckNumberOfDaysPerMonth(JAN, 31);
        CheckNumberOfDaysPerMonth(MAR, 31);
        CheckNumberOfDaysPerMonth(MAY, 31);
        CheckNumberOfDaysPerMonth(JUL, 31);
        CheckNumberOfDaysPerMonth(AUG, 31);
    }
 
    void ExceptFebruaryHas(int days)
    {
      CheckNumberOfDaysPerMonth(FEB, days);
    }
};
 
TEST(Date, non_leap_year_day_per_month_table)
{
    ThirtyDaysHasSeptEtc();
    ExceptFebruaryHas(28);
}
 
TEST(Date, leap_year_day_per_month_table)
{
    itsLeapYear();
    ThirtyDaysHasSeptEtc();
    ExceptFebruaryHas(28);
}

すべての隠蔽された部分へのアクセスが可能になったので、static関数IsLeapYear()、GetDaysInYear()、FirstSetYearAndDayOfYear()、ThenSetMonthAndDayOfMonth()、FinallySetDayOfWeek()に対してテストを書くことができる。

もしDateが抽象データ型であれば、データ構造体は.cファイルに隠蔽されているが、テストは隠蔽されたすべての構造体メンバに対してもアクセスすることができる。

このアプローチには欠点もある。今回のケースでは関係ないが、それが問題になる場合もある。このアプローチでは、複数のテストファイルで同一の.cファイルをインクルードすることができない。次の記事では、この問題を解決することにする。

なにか面白いうるう年バグについて聞いたことはあるだろうか?自分自身でうるう年バグを防いだことは?

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

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