[iOS] 日時に依存する機能のユニットテストを簡単に＆わかりやすく書けるカテゴリ


アプリを開発していると、現在日時が影響するような機能を作る機会が多いです。例えば...

* 時間限定でキャンペーンを表示したい
* クリスマス限定デザイン等の季節限定のイベントや機能

などなどですね。そんな機能を開発しているとき、テストケースを書く際に`NSDate`を`OCMock`であれこれがんばってたのですが、手間がかかったり、テストが見づらくなることが多かったです。

そこで、そんな<b>日時に関する機能のテストケースを書くのをサポートするNSDateのカテゴリ</b>を作ってみました！

## NSDate+SRGFekable

https://github.com/soragoto/NSDate-SRGFekable

## 導入してみる
Podfileに以下の行を追加します。

```ruby:Podfile
pod 'NSDate+SRGFekable'
```

さらに`Swift`＆テストプロジェクトで利用する場合は上に加えて`link_with`に以下の行を追加します。

```rb:Podfile
# swiftの場合必要、本プロジェクトとテストプロジェクトをリンクさせておく必要がある
# プロジェクトが"MyApp"だとすると..
link_with 'MyAppExample', 'MyAppTests'
````

`Podfile`更新後`pod update`したら、`Objective-C`の場合は利用するクラスで、`Swift`の場合は`Bridging-Header.h`で以下のファイルをインポートすると利用できます。

```objc
#import "NSDate+SRGFekable.h"
````



## なにができるの？

`NSDate`には現在時刻を元に値が決まるメソッドがありますが、それらの返り値を簡単に偽装(`fake`)することができます。
例えば利用例は以下のようになります。

```objc:簡単な日時の偽装
// 以下の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`の返す値が<b>fakeした値</b>になります。

（実装方法としては`fakeWithString`が呼ばれたタイミングで`Method Swizzling` を使って`NSDate`のメソッドを差し替えています。)

### 簡単な利用例 / UnitTestで使ってみる

このカテゴリを利用すると、内部で時刻に依存した処理があるようなクラスの`UnitTest`も簡単に書くことができます。

例えばテスト対象として`SeasonalEvent`クラスがあり、そこに`isXmas`というメソッドがあったとします。`isXmas`は日付が`12/25`の時のみ`TRUE`を返し、それ以外は`FALSE`を返す仕様だとすると...

`NSDate+SRGFekable.h`を利用して`XCTestCase`でテストケースを書くと以下のように書くことができます。

```objc:SeasonalEventのテストケース
....
- (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`となります。

```objc:fakeWithString
// タイムゾーンを指定しない場合、端末で設定されているタイムゾーンで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することもできます。

```objc:fakeWithDate
// 100秒後を指すNSDateを作成
NSDate *aDate = [NSDate dateWithTimeIntervalSinceNow:100];

// 現在時刻がaDateの指す時間になるようにfake
[NSDate fakeWithDate:aDate];

````

### fakeWithDelta - 差分時間でfake

`fakeWithDelta`を使うと、差分の時間を指定して`fake`することができます。

```objc:fakeWithDelta
// 現在時刻から60秒進める
[NSDate fakeWithDelta:60];

// さらに120秒進める( total 180秒 )
[NSDate fakeWithDelta:120];

// 30秒巻き戻す( total 150秒 )
[NSDate fakeWithDelta:-30];
````

### fake状態に関するメソッド

```objc:stopFaking
// fake状態を解除。NSDateを元の振る舞いに戻す
[NSDate stopFaking];

// fakeしているかどうかの状態を取得
BOOL doFaking = [NSDate doFaking];
````

### freezeオプション

上で紹介した３つの`fakeXXX`系メソッドは全て`freeze`という`BOOL`のオプションをつけることができます。`freeze`のデフォルト値は`YES`です。

`freeze`が`YES`の時、`fake`した時刻で時間は完全にとまっています。
`freeze`が`NO`の場合は、時間の経過に合わせて`fake`した時間が進みます。

これは`UnitTest`では、あまり利用しないかもしれませんが、実機での動作確認を`fake`を用いて行うことなどを想定しています。

```objc:freeze-option
// 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`も同梱しています。

```objc:Controllerの呼び出し方法
#import "SRGFakableViewController.h"
...
[SRGFakableViewController showOn:self freezeOnFake:NO];
````

引数`showOn:`には遷移元の`UIViewController`のインスタンスを渡します。なのでcontroller内から呼ぶ場合は`self`を渡しておけばOKです。  `freezeOnFake`の値は上で説明した`freeze`の挙動と同じで、偽装した時刻を停止させるかどうかのフラグです。

`showOn`が実行されると以下のような画面が表示されます。

![FakeFromControllerImage.png](https://qiita-image-store.s3.amazonaws.com/0/36581/f56cd49c-e148-9c3c-0c13-353feece88e6.png)

この画面から時刻を選択して`Do Fake!`ボタンをタップすると指定した日付で時刻が偽装されます。 閉じる時は左上の`Done`で画面を閉じれます。

（ただ、UI周りのライブラリなどのコア部分がNSDateに依存しているところがあるため、実際の動作で`NSDate`の振る舞いを固定すると、正常に動作しない場合もあると思います。）

## まとめ

以上、`NSDate`の振る舞いを変更することでテストを書きやすくするライブラリ`NSDate+SRGFekable`の紹介でした！ もし使えそうな場面があれば、ご利用ください。