使用 NSDateFormatter 避免脆弱的单元测试

Posted

技术标签:

【中文标题】使用 NSDateFormatter 避免脆弱的单元测试【英文标题】:Avoiding fragile unit tests with NSDateFormatter 【发布时间】:2013-11-27 09:59:44 【问题描述】:

测试 NSDateFormatter 方法的最佳实践是什么?例如,假设我有一个方法:

- (NSString *)formatStringFromDate:(NSDate *)date 
    NSDateFormatter *f = [[NSDateFormatter alloc] init];
    [f setTimeStyle:NSDateFormatterShortStyle];
    [f setDateStyle:NSDateFormatterNoStyle];

    return [f stringFromDate:date];

我可以想到两种使用 Kiwi 测试此方法的方法:

1)在单元测试中创建相同的格式化程序:

it(@"should format a date", ^
    NSDate *date = [NSDate date];
    NSDateFormatter *f = [[NSDateFormatter alloc] init];
    [f setTimeStyle:NSDateFormatterShortStyle];
    [f setDateStyle:NSDateFormatterNoStyle];

    [[[testObject formatStringFromDate:date] should] equal:[f stringFromDate:date]];
);

2) 明确写出预期的输出:

it(@"should format a date", ^
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:1385546122];
    NSDateFormatter *f = [[NSDateFormatter alloc] init];
    [f setTimeStyle:NSDateFormatterShortStyle];
    [f setDateStyle:NSDateFormatterNoStyle];

    [[[testObject formatStringFromDate:date] should] equal:@"9:55 am"];
);

现在,对我来说,#1 似乎有点多余。我知道测试会通过,因为我实际上是在单元测试中复制该方法。

方法#2 不适合,因为它非常脆弱。它完全依赖于您所期望的测试设备当前语言环境。

所以我的问题是:有没有更合适的方法来测试这个方法,或者我应该继续使用测试方法#1。

【问题讨论】:

测试的目的是什么?检查 NSDateFormatter 是否有效? 我实际上是在测试UITableViewCell 在存在日期属性时是否设置了detailTextLabel.text。我为我的问题简化了测试和方法。 你要测试的代码在哪里?在视图控制器或表格视图单元子类中? 它在 UITableViewController 子类中。 【参考方案1】:

正如您在评论中所说,您要测试的是单元格的 detailTextLabel 的文本设置为日期,并在存在日期时使用预期格式。

这可以通过几种不同的方式进行测试,我在这里公开我会采用的一种。

首先,每次我们想要格式化日期时都创建一个日期格式化程序效率不高,而且测试起来更加困难。

所以我的建议是在您的表格视图控制器中创建一个日期格式化程序属性:

// .h
@property (strong, nonatomic) NSDateFormatter *dateFormatter;

// .m
- (NSDateFormatter *) dateFormatter

    if (_dateFormatter == nil)
    
        _dateFormatter = [[NSDateFormatter alloc] init];
        [_dateFormatter setTimeStyle:NSDateFormatterShortStyle];
        [_dateFormatter setDateStyle:NSDateFormatterNoStyle];

    
    return _dateFormatter;

对于日期格式化程序,我将创建一个简单的测试来验证 timeStyle 和 dateStyle。我不熟悉 Kiwi,所以我使用 OCUnit 断言:

TableViewController *sut;// Instantiate the table view controller
STAssertTrue(sut.dateFormatter.timeStyle == NSDateFormatterShortStyle, nil);
STAssertTrue(sut.dateFormatter.dateStyle == setDateStyle:NSDateFormatterNoStyle, nil);

有了这个之后,想法是创建一个测试来验证tableView:cellForRowAtIndexPath: 返回的单元格的detailTextLabel 的文本是否与格式化程序返回的字符串一起设置。正如你所说,测试日期格式化程序返回的字符串是脆弱的,所以我们可以模拟它,存根 stringFromDate: 返回一个常量并验证 detailTextLabel 的文本是否设置为该常量。

所以我们编写测试。我会尝试用 Kiwi 模拟来编写它,如果我做错了什么,很抱歉 - 想法很重要:

TableViewController *sut;// Instantiate the table view controller

id mockDateFormatter = [NSDateFormatter mock];

NSString * const kFormattedDate = @"formattedDate";
NSDate * const date = [NSDate date];

[mockDateFormatter stub:@selector(stringFromDate:) andReturn:kFormattedDate withArguments:date,nil];

sut.dateFormatter = mockDateFormatter;
sut.dates = @[date];// As an example we have an array of dates to show. In the real case we would have an array of the objects you want to show in the table view.

[sut view];// In case we have the registered cells...
UITableViewCell *cell = [sut tableView:sut.tableView 
                 cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];

STAssertEqualObjects(cell.detailTextLabel.text, kFormattedDate, nil);

满足该测试的方法是这样的:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 

    // I assume you have a standard cell registered in your table view
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellIdentifier"];

    NSDate *date = [self.dates objectAtIndex:indexPath.row];
    cell.detailTextLabel.text = [self.dateFormatter stringFromDate:date];

    return cell;

希望对你有帮助。

【讨论】:

优秀的答案,正是我想要的。谢谢。 只是一个小提示:我会使用STAssertEquals(sut.dateFormatter.timeStyle, NSDateFormatterShortStyle, nil)。失败时,错误消息会显示 timeStyle 当前分配给日期格式化程序的内容,因此信息量更大。

以上是关于使用 NSDateFormatter 避免脆弱的单元测试的主要内容,如果未能解决你的问题,请参考以下文章

将程序集作为模块/插件加载,同时避免重复和脆弱性

使用Groovy+Spock轻松写出更简洁的单测

NSDateFormatter常见的使用方式

使用 NSDateFormatter 管理日期

NSDateFormatter:使用具有相对格式的自定义格式

带有 NSDateFormatter 的 RestKit