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ファイルをインクルードすることができない。次の記事では、この問題を解決することにする。

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