iOSのユニットテスト(LogicTests)で和暦設定時のテストを行う


自分の作ったアプリで和暦を使っていなくても、ユーザのカレンダー設定が和暦だったりすると
日付表示がおかしくなることがあります。
カレンダー設定を和暦に変更して実機での動作確認をすればいいのですが、いつもそれを気にして確認などはしないですし忘れがちです。

それに、設定によって挙動が変わってしまうのであれば、日付に関するテストコードを書いても意味がなくなってしまいます。

## 事前調査

iPhoneの設定でカレンダーを和暦に設定した上で、適当なアプリに`NSLog(@"%@", [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLocale"]);`
で記述して起動します。
そうすると、現在選択されているロケールが取得出来ます。

## OCMockを使って和暦設定時のロケールを返すMockを作成

OCMockを使い、先ほど取得したロケールを返すようにします。
いろいろ使い回せるように、下のように`SenTestCase`を継承したベースのテストケースを作成し、
和暦設定時のロケールのMockを作成するメソッドを追加します。

```objectivec:XXXTestCase.h
#import <SenTestingKit/SenTestingKit.h>

#import <OCMock/OCMock.h>

@interface XXXTestCase : SenTestCase

#pragma mark - NSLocale(ja_JP@calendar=japanese)

/*!
    @method createJPLocaleWithJapaneseCalendarMock
    @abstract iPhoneのカレンダー設定を和暦に書き換えたMockを作成
    @discussion
        iPhoneの[設定]-[一般]-[言語環境]-[カレンダー] = 和暦
 */
- (id)createJPLocaleWithJapaneseCalendarMock;

@end
```

```objectivec:XXXTestCase.m
@interface XXXTestCase ()

@property (nonatomic) NSMutableArray *mocksToVerify;

@end

@implementation XXXTestCase

- (void)tearDown
{
    for (id mock in self.mocksToVerify) {
        [mock verify];
    }
    self.mocksToVerify = nil;

    // [...]

    [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:XXXTestDomainName];

    [super tearDown];
}

#pragma mark - NSLocale(ja_JP@calendar=japanese)

- (id)createJPLocaleWithJapaneseCalendarMock
{
    NSLocale *locale = [NSLocale localeWithLocaleIdentifier:[NSString stringWithFormat:@"ja_JP@calendar=%@",
                                                             NSJapaneseCalendar]];
    id localeMock = [self autoVerifiedMockForClass:[NSLocale class]];
    [[[[localeMock stub] classMethod] andReturn:locale] systemLocale];
    [[[[localeMock stub] classMethod] andReturn:locale] currentLocale];
    [[[[localeMock stub] classMethod] andReturn:locale] autoupdatingCurrentLocale];

    return localeMock;
}

#pragma mark - Setup and verify mock: http://www.objc.io/issue-1/testing-view-controllers.html

- (void)verifyDuringTearDown:(id)mock
{
    if (!self.mocksToVerify) {
        self.mocksToVerify = @[].mutableCopy;
    }
    [self.mocksToVerify addObject:mock];
}

- (id)autoVerifiedMockForClass:(Class)aClass
{
    id mock = [OCMockObject mockForClass:aClass];
    [self verifyDuringTearDown:mock];
    return mock;
}

- (id)autoVerifiedPartialMockForObject:(id)object
{
    id mock = [OCMockObject partialMockForObject:object];
    [self verifyDuringTearDown:mock];
    return mock;
}

- (id)autoVerifiedNickMockForClass:(Class)aClass
{
    id mock = [OCMockObject niceMockForClass:aClass];
    [self verifyDuringTearDown:mock];
    return mock;
}

@end
```

## 和暦設定時のロケールを返すMockを利用したテストケース

`yyyyMM`の日付文字列を受け取って、それを年と月に分割した後、
'April, 2014'のような`MMMM, yyy`書式の文字列を変換する処理があり、
カレンダーが和暦設定だと、なぜか'April, 4002'となってしまうバグがあったとします。

それを確かめるテストコードは下記のようになります。

```objectivec
#import "XXXTestCase.h"

#import <OCMock.h>

@interface XXXDateConvertTests : XXXTestCase

@property (nonatomic) NSString *dateString;

@end

@implementation XXXDateConvertTests

- (void)setUp
{
    [super setUp];
    self.dateString = @"20140408";
}

- (void)tearDown
{
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

// MARK: 日付変換処理だけ抜き出した
- (NSString *)postedAtWithDateFormatter:(NSDateFormatter *)dateFormatter
{
    NSInteger year = [self.dateString substringWithRange:NSMakeRange(0, 4)].integerValue;
    NSInteger month = [self.dateString substringWithRange:NSMakeRange(4, 2)].integerValue;

    [dateFormatter setDateFormat:@"yyyy-MM-dd"];

    // MARK: 和暦設定の場合、年を正常にパースできないのでカレンダーをグレゴリオ暦に変更
//    dateFormatter.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];

    NSDate *date = [dateFormatter dateFromString:[NSString stringWithFormat:@"%i-%i-1", year, month]];

    [dateFormatter setLocale:[[NSLocale alloc]initWithLocaleIdentifier:@"en_US"]];
    [dateFormatter setDateFormat:@"MMMM, yyyy"];

    return [dateFormatter stringFromDate:date];
}

- (void)testPostedAt
{
    STAssertFalse([[[NSCalendar currentCalendar] calendarIdentifier] isEqualToString:NSJapaneseCalendar], @"");

    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    NSString *resultString = [self postedAtWithDateFormatter:[[NSDateFormatter alloc] init]];

    STAssertEqualObjects(resultString, @"April, 2014", @"");

    [self createJPLocaleWithJapaneseCalendarMock];  // 和暦設定に変更

    NSLog(@"%@", dateFormatter.locale.localeIdentifier);

    dateFormatter = [[NSDateFormatter alloc] init];
    resultString = [self postedAtWithDateFormatter:dateFormatter];
    STAssertFalse([resultString isEqualToString:@"April, 2014"], @"");  // April, 4002になってしまう
}

@end
```

## 参考

* [現在の言語とロケールを取得する](http://potting.syuriken.jp/potting_conv/BPInternational_J/chapter6.html)