タイムゾーン関係のソフトウェアテスト
タイムゾーン関係のソフトウェアテストの同値分析をしてみたいと思います。
タイムゾーンの説明の前に前提となる概念を簡単に説明します。
一般にコンピュータで時刻を表わすには基準時刻からの経過時間を数値で表現します。基準時はUTC(GMT)の1970年1月1日の0時0分です。この基準時をepoch(エポック)と呼ぶことから、基準時からの経過時間をエポック値と呼びます。
エポック値はタイムゾーンと独立した値で、常にUTCベースでの経過時間です。
伝統的にこの値(秒単位)をsigned intで持っていました。よく知られているように2038年にオーバーフローします。signed intなのでオーバーフローした時、飛ぶ先は1970年ではありません。どの年代に飛ぶかは自分で考えてみてください(このネタ、ありえるえりあで昔も書いた気がします)。
Javaのエポック値(単位はミリ秒)はlong型です。Javaに閉じていれば2038年問題はありません。
ソフトウェアで時刻を扱う時の原則のひとつは、内部的には可能な限りエポック値で時刻を保持することです。外部入出力時にローカルタイムと相互変換します。これは、文字コードを扱うプログラミングで、プログラム内では可能な限り内部文字コードを使い、必要な外部入出力時にのみ文字コード変換を集約させる原則に似ています。ただし、後述する「日付処理」があるので、内部にローカルタイムで処理する部分を抱え込む必要があります。この辺が(理想的には)入出力境界以外に外部文字コードが存在しない世界を作れる文字コード対応とは異なります。
ちなみに「UTCとGMTの違いは何ですか」というFAQがあります。以下を参考にしてください。今から説明する文脈ではUTCとGMTは等価です。
以下、UTCからローカルタイムへの変換をUTC=>LTと表記します。内部時刻から外部時刻への変換で多く起きるのでOutputのOを使い、UTC=>LT[O]と書くことにします。逆向きの変換をLT=>UTC[I]と表記します。たとえば日本時間(JST)であれば、UTC=>LT[O]では9時間の加算処理、LT=>UTC[I]では9時間の減算処理を意味します。
一般にUTCはエポック値(ただの数値)でLTはオブジェクト(Cであればtimeval構造体やtm構造体。JavaであればDateやCalendar)で持ちます。UTCの値を持ったオブジェクトを使うこともありますが、できれば避ける方が無難です。ローカルタイム値を持つエポック値は制御不能のバグをもたらすので絶対にやめるべきです。
タイムゾーン関係のありがちなバグを分類すると次のようになります。
- 変換漏れ
- 多重変換
- UTCで日処理
- ローカルタイムで時刻情報を削除
- 夏時間対応
- 祝日対応
変換漏れ
UTC=>LT[O]の変換漏れが起きると、現象としてユーザがUTCを目にすることになります。このバグはあまり起きません。なぜなら普通に日本人が日本で使っているだけで気づくからです。エスケープ処理では特別な文字がないと変換漏れに気づきませんが、タイムゾーン変換漏れは、どんな時刻でも気づくので、エスケープ処理に比べると見つけやすいバグです。
LT=>UTC[I]の変換漏れで、内部的にUTCであるべき値がLTになることがあります。表示時にLT=>LTの変換が起きて不正な時刻になるパターンはすぐに見つかるので大きな問題にはなりません。
内部値が表示に使われないパターンが問題です。たとえば内部でしか使わない更新時刻が(本来UTCであるべきなのに)LTになったとします。更新時刻を数値比較でソートしたり新旧判定をすることはよくあります。UTC値を持つ更新時刻とLT値を比較するとバグが表に出たり出なかったりします。と言うのも、日本で普通にテストしていると9時間以内の更新では不正な動作をして、9時間以上の間隔のある更新ではバグが現れないからです。
開発者がバグ報告から9時間以上経って再現させようとすると再現しないので、「再現しない」と報告者に突き返します。報告者が再現テストをしても、過去に登録したデータの表示テストだけをすると再現しなくなります。ここで、気のせいだったと諦めたらダメです。もう一度、更新から再現テストをやりなおす必要があります。
- 教訓
- 昔更新したデータを表示するだけで再現テストを終わりにせず、更新するところから再現テストをする
この教訓は、タイムゾーン限定ではなく時刻にまつわるすべてのテストに当てはまります。
両方向で変換漏れバグがあると、バグが隠れる可能性があります。入力時と出力時のタイムゾーンを変更してテストするとバグが顕在化します。
- 教訓
- 入力時と出力時に異なるタイムゾーンにしてテストする
多重変換
多重変換とは、既にUTC=>LT[O]してローカルタイムにした値をUTCのままのつもりで、もう一度UTC=>LT[O]するパターンのバグです。当然、逆向きでも起こりえます。
多重変換は文字コード変換やエスケープ処理でも起きがちなバグです。エスケープ処理では見つけにくいバグになりますが、タイムゾーン変換の場合はそれほどひどくありません。等しくどの時刻でもおかしな値になってくれるので、通常のブラックボックステストで見つかるからです。
UTCで日処理
タイムゾーンまわりのテストはここからが本番です。
「日処理」とは「日時(日付時刻)処理」のうち、時刻情報を除いた日付だけを対象にした処理のことです。代表的な日処理は曜日判定ですが、月や週や日の判定もあります。
日処理をUTCに対して行うことはバグです。日処理はローカルタイムの領域だからです。
これにまつわるバグは マルチスケジューラ にもありました。そして不思議なほど、長い間バグが見つからなかったので覚えています。普通に日本時間で作った予定が、普通に日本時間で見た時に日付が前日になってしまう現象でした。再現する条件は午前0時から午前9時の間のどこかに作った予定でした。内部的な原因は明白です。日本時間の午前0時から9時の間はUTCでは前日になるので、UTCで日判定をしていたバグでした。
こんな誰が見ても気づきそうなバグがなぜ長時間誰も気づかなかったのが不思議でしたが、日常会社で使っていると、午前0時から9時の間に予定を作ることがないのが原因でした。この10年間でこの時間帯に業務の予定が入ったことはありません。開発というのはそういうものです。
- 教訓
- 日本時間の午前0時から午前9時の間の時間帯をテストする
ローカルタイムで時刻情報を削除
ローカルタイムで時刻情報を落とすことで起きるタイムゾーン関係のバグがあります。白状すると、 マルチスケジューラ に既知のタイムゾーン関係のバグが残っているのですが、原因はこれです。
ローカルタイムで時刻情報を落とす理由は、終日の予定など日付情報だけで良い場合に、時刻情報のないことを終日フラグ代わりに使う手法です。手抜きだと思うかもしれませんが、 iCalendar (rfc2445)でもやっています。言い訳にはなりませんが。
ローカルタイムで日付情報を落とした時刻をUTCに変換すると、UTCのその日付の時刻0時になります。JST=>UTCして、再びUTC=>JSTした場合、同一日付の午前9時に戻ってきます。もともと日付しか用がなければ時刻情報を落としても同じ日付になるのでバグは表面化しません。しかしタイムゾーンの時差が負値の地域では日付が前日になりバグが顕在化します。
- 教訓
- どうせテストするなら、タイムゾーンの時差がマイナスの地域でテストする
夏時間対応
文字コード変換同様、タイムゾーン変換でも原則はライブラリを使うことです。普通のライブラリを使っている限り、夏時間対応で問題が発生することはありません。
手抜きをして自前で時差のオフセットの加減算処理を書いたりすると夏時間対応を漏らします。
ありがちなのが夏になるまで誰も気づかないというパターンです。もっとも、利用者も夏になるまで気づかないのですが。
普通の日本人に夏時間対応のテストはできないと思っていましたが、最近は中学生でもDaylight Saving Time(DST)という専門用語を知っているようです。意外に心配ないのかもしれません。
- 教訓
- テストする時、現在時刻を夏に変更してタイムゾーンをアメリカにしてテストする
祝日対応
祝日の日付をソースコードにハードコードしていたら問題外です。タイムゾーン対応の前にソースコードから祝日情報を外に追い出してください。
祝日情報を外部に追い出して切り替え可能にしたとしても、そもそもタイムゾーン切り替えで祝日を切り替えていいのかという問題が残ります。
Googleカレンダーとかどうしているのかと思って見てみたら、そもそもデフォルトで祝日はないようです。自分で祝日カレンダーを選択する必要があります。これが妥当な解決手法でしょうか。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/timezone/tbping