mno*_*rt9 39 tdd unit-testing objective-c ocunit ios
我真的很难理解单元测试.我确实理解TDD的重要性,但我读到的所有单元测试的例子似乎都非常简单和微不足道.例如,测试以确保设置属性或将内存分配给数组.为什么?如果我编码..alloc] init],我真的需要确保它有效吗?
我是开发新手,所以我肯定我在这里缺少一些东西,特别是围绕TDD的所有热潮.
我认为我的主要问题是我找不到任何实际的例子.这是一种setReminderId 似乎很适合测试的方法.什么是有用的单元测试看起来确保这是有效的?(使用OCUnit)
- (NSNumber *)setReminderId: (NSDictionary *)reminderData
{
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
// Increment the last reminderId
currentReminderId = @(currentReminderId.intValue + 1);
}
else {
// Set to 0 if it doesn't already exist
currentReminderId = @0;
}
// Update currentReminderId to model
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
Run Code Online (Sandbox Code Playgroud)
Jon*_*eid 95
更新:我已经通过两种方式改进了这个答案:它现在是一个截屏视频,我从属性注入切换到构造函数注入.请参阅如何开始使用Objective-C TDD
棘手的部分是该方法依赖于外部对象NSUserDefaults.我们不想直接使用NSUserDefaults.相反,我们需要以某种方式注入此依赖项,以便我们可以替换假的用户默认值进行测试.
有几种不同的方法可以做到这一点.一种是将其作为方法的额外参数传递.另一种方法是使它成为类的实例变量.设置这种伊娃有不同的方法.有"构造函数注入",它在初始化程序参数中指定.或者是"财产注入".对于iOS SDK中的标准对象,我的首选是使其成为具有默认值的属性.
因此,让我们从测试开始,默认情况下,该属性是NSUserDefaults.我的工具集,顺便说一句,是Xcode的内置的OCUnit,加上OCHamcrest的断言和OCMockito为模拟对象.还有其他选择,但这就是我使用的.
由于缺少更好的名字,该课程将被命名Example.该实例将以sut"受测试系统" 命名.该物业将被命名userDefaults.这是在ExampleTests.m中确定其默认值的第一个测试:
#import <SenTestingKit/SenTestingKit.h>
#define HC_SHORTHAND
#import <OCHamcrestIOS/OCHamcrestIOS.h>
@interface ExampleTests : SenTestCase
@end
@implementation ExampleTests
- (void)testDefaultUserDefaultsShouldBeSet
{
Example *sut = [[Example alloc] init];
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
@end
Run Code Online (Sandbox Code Playgroud)
在这个阶段,这不会编译 - 这将被视为测试失败.仔细看看.如果你能让你的眼睛跳过括号和括号,测试应该非常清楚.
让我们编写最简单的代码来获得编译和运行的测试 - 并且失败.这是Example.h:
#import <Foundation/Foundation.h>
@interface Example : NSObject
@property (strong, nonatomic) NSUserDefaults *userDefaults;
@end
Run Code Online (Sandbox Code Playgroud)
而令人敬畏的例子:
#import "Example.h"
@implementation Example
@end
Run Code Online (Sandbox Code Playgroud)
我们需要在ExampleTests.m的最开头添加一行:
#import "Example.h"
Run Code Online (Sandbox Code Playgroud)
测试运行,并且失败并显示消息"预期NSUserDefaults的实例,但是为零".正是我们想要的.我们已经完成了第一次测试的第一步.
第2步是编写我们可以通过该测试的最简单的代码.这个怎么样:
- (id)init
{
self = [super init];
if (self)
_userDefaults = [NSUserDefaults standardUserDefaults];
return self;
}
Run Code Online (Sandbox Code Playgroud)
它过去了!第2步完成.
第3步是重构代码以包含生产代码和测试代码中的所有更改.但是还没有什么可以清理的.我们完成了第一次测试.到目前为止我们有什么?可以访问的类的开头NSUserDefaults,但也可以覆盖它以进行测试.
现在让我们为该方法编写一个测试.我们想要它做什么?如果用户默认值没有匹配的键,我们希望它返回0.
当我第一次使用模拟对象时,我建议首先手工制作它们,这样你就可以了解它们的用途.然后开始使用模拟对象框架.但是我要继续前进并使用OCMockito来加快速度.我们将这些行添加到ExampleTest.m:
#define MOCKITO_SHORTHAND
#import <OCMockitoIOS/OCMockitoIOS.h>
Run Code Online (Sandbox Code Playgroud)
默认情况下,基于OCMockito的模拟对象将返回nil任何方法.但是我会编写额外的代码来明确期望,说:"鉴于它被要求objectForKey:@"currentReminderId",它将会返回nil." 考虑到所有这些,我们希望该方法返回NSNumber 0.(我不会传递参数,因为我不知道它的用途.我将命名该方法nextReminderId.)
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
Example *sut = [[Example alloc] init];
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
Run Code Online (Sandbox Code Playgroud)
这还没有编译.让我们nextReminderId在Example.h中定义方法:
- (NSNumber *)nextReminderId;
Run Code Online (Sandbox Code Playgroud)
这是Example.m中的第一个实现.我希望测试失败,所以我要返回一个伪造的数字:
- (NSNumber *)nextReminderId
{
return @-1;
}
Run Code Online (Sandbox Code Playgroud)
测试失败,并显示消息"预期<0>,但是<-1>".测试失败很重要,因为它是我们测试测试的方式,并确保我们编写的代码将其从失败状态转换为传递状态.第1步完成.
第2步:让我们通过测试测试.但请记住,我们想要通过测试的最简单的代码.它看起来非常愚蠢.
- (NSNumber *)nextReminderId
{
return @0;
}
Run Code Online (Sandbox Code Playgroud)
太棒了,它过去了!但我们还没有完成这项测试.现在我们来到第3步:重构.测试中有重复的代码.让我们把sut被测系统拉进伊娃.我们将使用该-setUp方法进行设置,并-tearDown进行清理(销毁它).
@interface ExampleTests : SenTestCase
{
Example *sut;
}
@end
@implementation ExampleTests
- (void)setUp
{
[super setUp];
sut = [[Example alloc] init];
}
- (void)tearDown
{
sut = nil;
[super tearDown];
}
- (void)testDefaultUserDefaultsShouldBeSet
{
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
@end
Run Code Online (Sandbox Code Playgroud)
我们再次运行测试,以确保它们仍然通过,他们确实通过了.重构只能在"绿色"或通过状态下进行.无论是在测试代码还是生产代码中进行重构,所有测试都应继续通过.
现在让我们测试另一个要求:应该保存用户默认值.我们将使用与先前测试相同的条件.但是我们创建了一个新测试,而不是在现有测试中添加更多断言.理想情况下,每个测试都应验证一件事,并且要有一个好的名称来匹配.
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}
Run Code Online (Sandbox Code Playgroud)
该verify声明是OCMockito的说法,"这个模拟对象应该这样被称为一次." 我们运行测试并失败,"预期1匹配调用,但收到0".第1步完成.
第2步:通过的最简单的代码.准备?开始:
- (NSNumber *)nextReminderId
{
[_userDefaults setObject:@0 forKey:@"currentReminderId"];
return @0;
}
Run Code Online (Sandbox Code Playgroud)
"但是为什么要保存@0用户默认值,而不是那个带有该值的变量?" 你问.因为这是我们测试过的.坚持下去,我们会到达那里.
第3步:重构.同样,我们在测试中有重复的代码.让我们mockUserDefaults像伊娃一样拉出来.
@interface ExampleTests : SenTestCase
{
Example *sut;
NSUserDefaults *mockUserDefaults;
}
@end
Run Code Online (Sandbox Code Playgroud)
测试代码显示警告,"mockUserDefaults'的本地声明隐藏了实例变量".修复它们以使用ivar.然后让我们提取一个辅助方法,以在每个测试开始时建立用户默认值的条件.让我们把它nil拉出一个单独的变量来帮助我们进行重构:
NSNumber *current = nil;
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];
Run Code Online (Sandbox Code Playgroud)
现在选择最后3行,上下文单击,然后选择Refactor▶Extract.我们将创建一个名为的新方法setUpUserDefaultsWithCurrentReminderId:
- (void)setUpUserDefaultsWithCurrentReminderId:(NSNumber *)current
{
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];
}
Run Code Online (Sandbox Code Playgroud)
现在调用它的测试代码如下所示:
NSNumber *current = nil;
[self setUpUserDefaultsWithCurrentReminderId:current];
Run Code Online (Sandbox Code Playgroud)
这个变量的唯一原因是帮助我们进行自动重构.让我们把它拼凑起来:
[self setUpUserDefaultsWithCurrentReminderId:nil];
Run Code Online (Sandbox Code Playgroud)
测试仍然通过.由于Xcode的自动重构没有通过调用新的辅助方法替换该代码的所有实例,因此我们需要自己完成.所以现在测试看起来像这样:
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
[self setUpUserDefaultsWithCurrentReminderId:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}
Run Code Online (Sandbox Code Playgroud)
看看我们如何继续清洁?测试实际上变得更容易阅读!
现在我们要测试一下,如果用户默认值有一些值,我们返回一个更大的值.我将复制并更改"应该返回零"测试,使用任意值3.
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldReturnOneGreater
{
[self setUpUserDefaultsWithCurrentReminderId:@3];
assertThat([sut nextReminderId], is(equalTo(@4)));
}
Run Code Online (Sandbox Code Playgroud)
根据需要失败:"预期<4>,但是<0>".
这是传递测试的简单代码:
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
reminderId = @([reminderId integerValue] + 1);
else
reminderId = @0;
[_userDefaults setObject:@0 forKey:@"currentReminderId"];
return reminderId;
}
Run Code Online (Sandbox Code Playgroud)
除此之外setObject:@0,这开始看起来像你的例子.我还没有看到任何重构的东西.(实际上有,但直到后来才注意到.让我们继续.)
现在我们可以再建一个测试:给定相同的条件,它应该在用户默认值中保存新的提醒ID.这可以通过复制早期测试,更改它并给它一个好名字来快速完成:
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldSaveOneGreaterInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:@3];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@4 forKey:@"currentReminderId"];
}
Run Code Online (Sandbox Code Playgroud)
该测试失败,"预期1匹配调用,但收到0".当然,为了让它通过,我们只需将其更改setObject:@0为setObject:reminderId.一切都过去了.我们完成了!
等等,我们还没完成.第3步:有什么可以重构的吗?当我第一次写这篇文章时,我说,"不是真的." 但看完" 清洁密码"第3集后,我可以听到鲍勃叔叔告诉我,"一个功能应该有多大?4条线路可以,也许是5条线路......好吧.10条太大了." 这是7行.我错过了什么?它必须通过做一件以上的事情来违反职能规则.
再一次,鲍勃叔叔:"真正确定一个功能做一件事的唯一方法就是提取'直到你掉落'." 前4行一起工作; 他们计算实际价值.让我们选择它们,然后重构▶提取.遵循Bob叔叔第2集的范围规则,我们将给它一个很好的,长描述性的名称,因为它的使用范围非常有限.以下是自动重构为我们提供的:
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
reminderId = @([reminderId integerValue] + 1);
else
reminderId = @0;
return reminderId;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId;
reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:@"currentReminderId"];
return reminderId;
}
Run Code Online (Sandbox Code Playgroud)
让我们把它清理干净吧:
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
return @([reminderId integerValue] + 1);
else
return @0;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:@"currentReminderId"];
return reminderId;
}
Run Code Online (Sandbox Code Playgroud)
现在每种方法都非常紧凑,任何人都可以轻松阅读主要方法的3行,看看它的作用.但是,如果用户默认密钥分布在两种方法中,我会感到很不舒服.让我们将它提取到Example.m头部的常量:
static NSString *const currentReminderIdKey = @"currentReminderId";
Run Code Online (Sandbox Code Playgroud)
我将在生产代码中出现该密钥的任何地方使用该常量.但测试代码继续使用文字.这可以防止我们意外地改变那个恒定键.
所以你有它.在五个测试中,我已经使用TDD来查找您要求的代码.希望它能让您更清楚地了解如何进行TDD,以及为什么它值得.按照3步华尔兹
你不只是在同一个地方.你最终得到:
所有这些好处将比投入TDD的时间节省更多时间 - 而不仅仅是长期,而是立即.
有关完整应用程序的示例,请阅读" 测试驱动的iOS开发 "一书.这是我对这本书的评论.
| 归档时间: |
|
| 查看次数: |
10984 次 |
| 最近记录: |