アプリを開発していると、現在日時が影響するような機能を作る機会が多いです。例えば...
- 時間限定でキャンペーンを表示したい
- クリスマス限定デザイン等の季節限定のイベントや機能
などなどですね。そんな機能を開発しているとき、テストケースを書く際にNSDate
をOCMock
であれこれがんばってたのですが、手間がかかったり、テストが見づらくなることが多かったです。
そこで、そんな日時に関する機能のテストケースを書くのをサポートするNSDateのカテゴリを作ってみました!
NSDate+SRGFekable
導入してみる
Podfileに以下の行を追加します。
pod 'NSDate+SRGFekable'
さらにSwift
&テストプロジェクトで利用する場合は上に加えてlink_with
に以下の行を追加します。
# swiftの場合必要、本プロジェクトとテストプロジェクトをリンクさせておく必要がある
# プロジェクトが"MyApp"だとすると..
link_with 'MyAppExample', 'MyAppTests'
Podfile
更新後pod update
したら、Objective-C
の場合は利用するクラスで、Swift
の場合はBridging-Header.h
で以下のファイルをインポートすると利用できます。
#import "NSDate+SRGFekable.h"
なにができるの?
NSDate
には現在時刻を元に値が決まるメソッドがありますが、それらの返り値を簡単に偽装(fake
)することができます。
例えば利用例は以下のようになります。
// 以下の1行を追加すると...
[NSDate fakeWithString:@"2014/12/27 10:00:00"];
// これ以降プロセス内でNSDateで取得する現在時刻が上で偽装(fake)した日付に
NSLog(@"now:%@",[NSDate date]); // -> now:2014/12/27 10:00:00
上のコードのようにfakeWithString
を呼ぶと、現在時刻に依存するNSDate
のメソッド+date
、+dateWithTimeIntervalSinceNow:
、-initWithTimeIntervalSinceNow:
、-init
、-timeIntervalSinceNow
の返す値がfakeした値になります。
(実装方法としてはfakeWithString
が呼ばれたタイミングでMethod Swizzling
を使ってNSDate
のメソッドを差し替えています。)
簡単な利用例 / UnitTestで使ってみる
このカテゴリを利用すると、内部で時刻に依存した処理があるようなクラスのUnitTest
も簡単に書くことができます。
例えばテスト対象としてSeasonalEvent
クラスがあり、そこにisXmas
というメソッドがあったとします。isXmas
は日付が12/25
の時のみTRUE
を返し、それ以外はFALSE
を返す仕様だとすると...
NSDate+SRGFekable.h
を利用してXCTestCase
でテストケースを書くと以下のように書くことができます。
....
- (void)testIsXmas {
SeasonalEvent *seasonalEvent = [SeasonalEvent new];
// 12/20はクリスマスではない
[NSDate fakeWithString:@"2014/12/20 10:00:00"];
XCTAssertFalse( seasonalEvent.isXmas );
// 12/25はクリスマス
[NSDate fakeWithString:@"2014/12/25 10:00:00"];
XCTAssertTrue( seasonalEvent.isXmas );
}
...
OCMock
などでmock
を組み立てるよりもシンプルにかけると思います。
fakeWithString
以外にも、NSDate
周りのテストを、より簡単に行えるメソッドを用意しているので、以下で詳細を説明します。
詳細な使い方
fakeWithString - 文字列からfake
fakeWithString
を使うと文字列で日付を指定できます。timeZone
はデフォルトの場合はシステムで利用しているtimeZone
となります。
// タイムゾーンを指定しない場合、端末で設定されているタイムゾーンでfake
[NSDate fakeWithString:@"2014/12/20 10:00:00"];
// タイムゾーンを明示的に指定することもできます
[NSDate fakeWithString:@"2014/12/26 10:00:00"
timeZone:[NSTimeZone timeZoneWithName:@"Asia/Tokyo"]
];
fakeWithDate - NSDateインスタンスからfake
fakeWithDate を使うと
NSDate`のインスタンスをもとにfakeすることもできます。
// 100秒後を指すNSDateを作成
NSDate *aDate = [NSDate dateWithTimeIntervalSinceNow:100];
// 現在時刻がaDateの指す時間になるようにfake
[NSDate fakeWithDate:aDate];
fakeWithDelta - 差分時間でfake
fakeWithDelta
を使うと、差分の時間を指定してfake
することができます。
// 現在時刻から60秒進める
[NSDate fakeWithDelta:60];
// さらに120秒進める( total 180秒 )
[NSDate fakeWithDelta:120];
// 30秒巻き戻す( total 150秒 )
[NSDate fakeWithDelta:-30];
fake状態に関するメソッド
// fake状態を解除。NSDateを元の振る舞いに戻す
[NSDate stopFaking];
// fakeしているかどうかの状態を取得
BOOL doFaking = [NSDate doFaking];
freezeオプション
上で紹介した3つのfakeXXX
系メソッドは全てfreeze
というBOOL
のオプションをつけることができます。freeze
のデフォルト値はYES
です。
freeze
がYES
の時、fake
した時刻で時間は完全にとまっています。
freeze
がNO
の場合は、時間の経過に合わせてfake
した時間が進みます。
これはUnitTest
では、あまり利用しないかもしれませんが、実機での動作確認をfake
を用いて行うことなどを想定しています。
// freeze:YESにすると、指定した時刻のまま常に同じ時刻を返す(デフォルトの動作)
[NSDate fakeWithString:@"2014/12/27 10:00:00" freeze:YES];
// -> 上の処理から10秒たっても[NSDate date]の値は10時ジャストのまま
// freeze:NOにすると、指定から時間が進んでいく
[NSDate fakeWithString:@"2014/12/27 10:00:00" freeze:NO];
// -> 上の処理から10秒たつと[NSDate date]の値は10:00:10になる
おまけ - 単体テスト以外でも使ってみる
UnitTest
の利用がメインだと思いますが、実機での動作確認に使うこともできます。
その際に、GUIで日付が変えれる機能があると便利だと思い、変更用のController
も同梱しています。
#import "SRGFakableViewController.h"
...
[SRGFakableViewController showOn:self freezeOnFake:NO];
引数showOn:
には遷移元のUIViewController
のインスタンスを渡します。なのでcontroller内から呼ぶ場合はself
を渡しておけばOKです。 freezeOnFake
の値は上で説明したfreeze
の挙動と同じで、偽装した時刻を停止させるかどうかのフラグです。
showOn
が実行されると以下のような画面が表示されます。
この画面から時刻を選択してDo Fake!
ボタンをタップすると指定した日付で時刻が偽装されます。 閉じる時は左上のDone
で画面を閉じれます。
(ただ、UI周りのライブラリなどのコア部分がNSDateに依存しているところがあるため、実際の動作でNSDate
の振る舞いを固定すると、正常に動作しない場合もあると思います。)
まとめ
以上、NSDate
の振る舞いを変更することでテストを書きやすくするライブラリNSDate+SRGFekable
の紹介でした! もし使えそうな場面があれば、ご利用ください。