测试驱动开发(Test Driven Development,以下简称TDD),TDD是敏捷开发中的一项核心实践和技术,也是一种设计方法论。原理呢,是在开发功能代码之前,先编写单元测试用例代码,测试代码是要根据需求的产品来编写的代码。TDD的基本思路就是通过测试来推动整个开发的进行。测试驱动开发不是简单的测试,是需要把需求分析、设计和质量控制量化的过程。测试驱动开发就是,在了解需求功能之后,制定了一套测试用例代码,这套测试用例代码对你的需求(对象、功能、过程、接口等)进行设计,测试框架可以持续进行验证。就像是在画画之前先画好了基本的轮廓,来保证能够画成你想要的东西。
行为驱动开发( Behavior Driven Development,以下简称BDD), BDD是在应用程序存在之前,写出用例与期望,从而描述应用程序的行为,并且促使在项目中的人们彼此互相沟通。BDD关注的是业务领域,而不是技术。BDD强调用领域特定语言描述用户行为,定义业务需求,让开发者集中精力于代码的写法而不是技术细节上。着重在整个开发层面所有参与者对行为和业务的理解。行为驱动开发将所有人集中在一起用一种特定的语言将所需要的系统行为形成一个一致理解认可的术语。就像是统一了的普通话,各个地区的人可以通过普通话来了解一句话意义是什么。
作为第二代敏捷方法,BDD提倡的是通过将测试语句转换为类似自然语言的描述,开发人员可以使用更符合大众语言的习惯来书写测试,这样不论在项目交接/交付,或者之后自己修改时,都可以顺利很多。如果说作为开发者的我们日常工作是写代码,那么BDD其实就是在讲故事。一个典型的BDD的测试用例包活完整的三段式上下文,测试大多可以翻译为Given..When..Then的格式,读起来轻松惬意。BDD在其他语言中也已经有一些框架,包括最早的Java的JBehave和赫赫有名的Ruby的RSpec和Cucumber。而在objc社区中BDD框架也正在欣欣向荣地发展,得益于objc的语法本来就非常接近自然语言,再加上C语言宏的威力,我们是有可能写出漂亮优美的测试的。在objc中,现在比较流行的BDD框架有cedar,specta和Kiwi。本文主要介绍的是Kiwi,使用Kiwi写出的测试看起来大概会是这个样子的:
示例如下所示:
describe(@"Team", ^{
context(@"when newly created", ^{
it(@"has a name", ^{
id team = [Team team];
[[team.name should] equal:@"Black Hawks"];
});
it(@"has 11 players", ^{
id team = [Team team];
[[[team should] have:11] players];
});
});
});
我们很容易根据上下文将其提取为Given..When..Then的三段式自然语言
Given a team, when newly created, it should have a name, and should have 11 players
很简单啊有木有!在这样的语法下,是不是写测试的兴趣都被激发出来了呢。关于Kiwi的进一步语法和使用,我们稍后详细展开。首先来看看如何在项目中添加Kiwi框架吧。
可以通过通过CocoaPods安装,请将此添加到您的Podfile:
pod "Kiwi"
点击下载Demo:ZJHUnitTestDemo
可以直接创建一个普通的Objective-C test case class,如:ZJHFirstKiwiTests,然后再里面添加Kiwi代码:
#import "Kiwi.h"
SPEC_BEGIN(SimpleStringSpec)
describe(@"SimpleString", ^{
context(@"when assigned to 'Hello world'", ^{
NSString *greeting = @"Hello world";
it(@"should exist", ^{
[[greeting shouldNot] beNil];
});
it(@"should equal to 'Hello world'", ^{
[[greeting should] equal:@"Hello world"];
});
});
});
SPEC_END
你可能会觉得这不是objc代码,甚至怀疑这些语法是否能够编译通过。其实SPEC_BEGIN和SPEC_END都是宏,它们定义了一个KWSpec的子类,并将其中的内容包装在一个函数中(有兴趣的朋友不妨点进去看看)。
describe描述需要测试的对象内容,也即我们三段式中的Given,context描述测试上下文,也就是这个测试在When来进行,最后it中的是测试的本体,描述了这个测试应该满足的条件,三者共同构成了Kiwi测试中的行为描述。它们是可以nest的,也就是一个Spec文件中可以包含多个describe(虽然我们很少这么做,一个测试文件应该专注于测试一个类);一个describe可以包含多个context,来描述类在不同情景下的行为;一个context可以包含多个it的测试例。让我们运行一下这个测试,观察输出:
ZJHUnitTestDemo[14459:288758] + 'SimpleString, when assigned to 'Hello world', should exist' [PASSED]
ZJHUnitTestDemo[14459:288758] + 'SimpleString, when assigned to 'Hello world', should equal to 'Hello world'' [PASSED]
先看下面的第二个示例子代码
#import "Kiwi.h"
#import "ZJHKiwiSample.h"
// SPEC_BEGIN(ClassName) 和 SPEC_END 宏,用于标记 KWSpec 类的开始和结束,以及测试用例的分组声明
SPEC_BEGIN(ZJHKiwiSampleSpec)
describe(@"ZJHKiwiSample Kiwi test", ^{
registerMatchers(@"ZJH"); // 注册所有使用"ZJH"命名空间前缀的匹配器.
context(@"a state the component is in", ^{
let(variable, ^{ // 在每个包含的 "it" 执行前执行执行一次.
return [[ZJHKiwiSample alloc]init];
});
beforeAll(^{ // 在所有内嵌上下文或当前上下文的 it block执行之前执行一次.
NSLog(@"beforAll");
});
afterAll(^{ // 在所有内嵌上下文或当前上下文的 it block执行之后执行一次.
NSLog(@"afterAll");
});
beforeEach(^{ // 在所有包含的上下文环境的 it block执行之前,均各执行一次.用于初始化指定上下文环境的代码
NSLog(@"beforeEach");
});
afterEach(^{ // 在所有包含的上下文环境的 it block执行之后,均各执行一次.
NSLog(@"afterEach");
});
it(@"should do something", ^{ // 声明一个测试用例.这里描述了对对象或行为的期望.
NSLog(@"should do something");
});
specify(^{ // 可用于标记尚未完成的功能或用例,仅会使Xcode输出一个黄色警告
NSLog(@"specify");
[[variable shouldNot] beNil];
});
context(@"inner context", ^{ // 可以嵌套context
NSLog(@"inner context");
it(@"does another thing", ^{
NSLog(@"does another thing");
});
pending(@"等待实现的东西", ^{ // 可用于标记尚未完成的功能或用例,仅会使Xcode输出一个黄色警告
NSLog(@"等待实现的东西");
});
});
});
});
SPEC_END
#import "Kiwi.h" 导入Kiwi库.这应该在规则的文件开始处最先导入.SPEC_BEGIN(ClassName) 和 SPEC_END 宏,用于标记 KWSpec 类的开始和结束,以及测试用例的分组声明.registerMatchers(aNamespacePrefix) 注册所有使用指定命名空间前缀的匹配器.除了Kiwi默认的匹配器,这些匹配器也可以在当前规则中使用.describe(aString, aBlock) 开启一个上下文环境,可包含测试用例或嵌套其他的上下文环境.beforeAll(aBlock) 在所有内嵌上下文或当前上下文的``it`block执行之前执行一次.afterAll(aBlock) 在所有内嵌上下文或当前上下文的``it`block执行之后执行一次.beforeEach(aBlock) 在所有包含的上下文环境的 itblock执行之前,均各执行一次.用于初始化指定上下文环境的代码,应该放在这里.afterEach(aBlock) 在所有包含的上下文环境的 itblock执行之后,均各执行一次.it(aString, aBlock) 声明一个测试用例.这里描述了对对象或行为的期望.specify(aBlock) 声明一个没有描述的测试用例.这个常用于简单的期望.pending(aString, aBlock) 可用于标记尚未完成的功能或用例,仅会使Xcode输出一个黄色警告.(有点TODO的赶脚)let(subject, aBlock) 声明一个本地工具变量,这个变量会在规则内所有上下文的每个 itblock执行前,重新初始化一次.期望(Expectations),用来验证用例中的对象行为是否符合你的语气。期望相当于传统测试中的断言,要是运行的结果不能匹配期望,则测试失败。在Kiwi中期望都由should或者shouldNot开头,并紧接一个或多个判断的的链式调用,大部分常见的是be或者haveSomeCondition的形式。在我们上面的例子中我们使用了should not be nil和should equal两个期望来确保字符串赋值的行为正确。一个期望,具有如下形式: [[subject should] someCondition:anArgument].此处 [subject should]是表达式的类型, ... someCondition:anArgument] 是匹配器的表达式。如下示例
// 可以用下面的内容替换原来的tests.m中的内容,然后cmd+u
// 测试失败可自行解决;解决不了的,继续往下看.
#import "Kiwi.h"
#import "ZJHKiwiCar.h"
SPEC_BEGIN(ZJHExpectationKiwiSpec)
describe(@"YFKiwiCar Test", ^{
it(@"A Car Rule", ^{
id car = [ZJHKiwiCar new];
[[car shouldNot] beNil]; // car对象不能为nil
[[car should] beKindOfClass:[ZJHKiwiCar class]]; // 应该是ZJHKiwiCar类
[[car shouldNot] conformToProtocol:@protocol(NSCopying)]; // 应该没有实现NSCopying协议
[[[car should] have:4] wheels]; // 应该有4个轮子
[[theValue([(ZJHKiwiCar *)car speed]) should] equal:theValue(42.0f)]; // 测速应该是42
[[car should] receive:@selector(changeToGear:) withArguments: theValue(3)]; // 接收的参数应该是3
[car changeToGear: 3]; // 调用方法
});
});
SPEC_END
[subject should] 和 [subject shouldNot] 表达式,类似于一个接收器,用于接收一个期望匹配器.他们后面紧跟的是真实的匹配表达式,这些表达式将真正被用于计算.
默认地,主语守卫(一种机制,可以保证nil不引起崩溃)也会在[subject should ]和 [subject shouldNot]被使用时创建.给 nil 发送消息,通常不会有任何副作用.但是,你几乎不会希望:一个表达式,只是为了给某个对象传递一个无足轻重的消息,就因为对象本身是nil.也就说,向nil对象本身发送消息,并不会有任何副作用;但是在BBD里,某个要被传递消息的对象是nil,通常是非预期行为.所以,这些表达式的对象守卫机制,会将左侧无法判定为不为nil的表达式判定为 fail失败.
"装箱"是固定术语译法,其实即使我们iOS常说的基本类型转NSObject类型(事实如此,勿喷)。部分表达式中,匹配器表达式的参数总是NSObject对象.当将一个标量(如int整型,float浮点型等)用于需要id类型参数的地方时,应使用theValue(一个标量)宏将标量装箱.这种机制也适用于: 当一个标量需要是一个表达式的主语(主谓宾,基本语法规则,请自行脑补)时,或者一个 存根 的值需要是一个标量时.
it(@"Scalar packing",^{ // 标量装箱
[[theValue(1 + 1) should] equal:theValue(2)];
[[theValue(YES) shouldNot] equal:theValue(NO)];
[[theValue(20u) should] beBetween:theValue(1) and:theValue(30.0)];
ZJHKiwiCar * car = [ZJHKiwiCar new];
[[theValue(car.speed) should] beGreaterThan:theValue(40.0f)];
});
在iOS中,常将调用某个实例对象的方法成为给这个对象发送了某个消息.所以"消息模式"中的"消息",更多的指的的实例对象的方法;"消息模式"也就被用来判断对象的某个方法是否会调用以及是否会按照预期的方式调用。一些 Kiwi 匹配器支持使用消息模式的期望.消息模式部分,常被放在一个表达式的后部,就像一个将要发给主语的消息一样.
it(@"Message Pattern", ^{ // 消息模式
ZJHKiwiCar *cruiser = [[ZJHKiwiCar alloc]init];
[[cruiser should] receive:@selector(jumpToStarSystemWithIndex:) withArguments: theValue(3)];
[cruiser jumpToStarSystemWithIndex: 3]; // 期望传的参数是3
});
[[subject shouldNot] beNil]
[[subject should] beNil]
[[subject should] beIdenticalTo:(id)anObject] - 比较是否完全相同
[[subject should] equal:(id)anObject]
[[subject should] equal:(double)aValue withDelta:(double)aDelta]
[[subject should] beWithin:(id)aDistance of:(id)aValue]
[[subject should] beLessThan:(id)aValue]
[[subject should] beLessThanOrEqualTo:(id)aValue]
[[subject should] beGreaterThan:(id)aValue]
[[subject should] beGreaterThanOrEqualTo:(id)aValue]
[[subject should] beBetween:(id)aLowerEndpoint and:(id)anUpperEndpoint]
[[subject should] beInTheIntervalFrom:(id)aLowerEndpoint to:(id)anUpperEndpoint]
[[subject should] beTrue]
[[subject should] beFalse]
[[subject should] beYes]
[[subject should] beNo]
[[subject should] beZero]
[[subject should] containString:(NSString*)substring]
[[subject should] containString:(NSString*)substring options:(NSStringCompareOptions)options]
[[subject should] startWithString:(NSString*)prefix]
[[subject should] endWithString:(NSString*)suffix]
示例:
[[@"Hello, world!" should] containString:@"world"];
[[@"Hello, world!" should] containString:@"WORLD" options:NSCaseInsensitiveSearch];
[[@"Hello, world!" should] startWithString:@"Hello,"];
[[@"Hello, world!" should] endWithString:@"world!"];
[[subject should] matchPattern:(NSString*)pattern]
[[subject should] matchPattern:(NSString*)pattern options:(NSRegularExpressionOptions)options]
示例:
[[@"ababab" should] matchPattern:@"(ab)+"];
[[@" foo " shouldNot] matchPattern:@"^foo$"];
[[@"abABab" should] matchPattern:@"(ab)+" options:NSRegularExpressionCaseInsensitive];
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; }]
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; } by:+1]
[[theBlock(^{ ... }) should] change:^{ return (NSInteger)count; } by:-1]
示例:
it(@"Expectations: Count changes", ^{ // 期望: 数量的变化
NSMutableArray * array = [NSMutableArray arrayWithCapacity: 42];
[[theBlock(^{ // 数量应该+1
[array addObject:@"foo"];
}) should] change:^{
return (NSInteger)[array count];
} by:+1];
[[theBlock(^{ // 数量不应该改变
[array addObject:@"bar"];
[array removeObject:@"foo"];
}) shouldNot] change:^{ return (NSInteger)[array count]; }];
[[theBlock(^{ // 数量应该-1
[array removeObject:@"bar"];
}) should] change:^{ return (NSInteger)[array count]; } by:-1];
});
[[subject should] beKindOfClass:(Class)aClass]
[[subject should] beMemberOfClass:(Class)aClass]
[[subject should] conformToProtocol:(Protocol *)aProtocol]
[[subject should] respondToSelector:(SEL)aSelector]
对于集合主语(即,主语是集合类型的):
[[subject should] beEmpty]
[[subject should] contain:(id)anObject]
[[subject should] containObjectsInArray:(NSArray *)anArray]
[[subject should] containObjects:(id)firstObject, ...]
[[subject should] haveCountOf:(NSUInteger)aCount]
[[subject should] haveCountOfAtLeast:(NSUInteger)aCount]
[[subject should] haveCountOfAtMost:(NSUInteger)aCount]
对于集合键(即此属性/方法名对应/返回一个集合类型的对象):
[[[subject should] have:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtLeast:(NSUInteger)aCount] collectionKey]
[[[subject should] haveAtMost:(NSUInteger)aCount] collectionKey]
如果主语是一个集合(比如 NSArray数组), coollectionKey 可以是任何东西(比如 items),只要遵循语法结构就行.否则, coollectionKey应当是一个可以发送给主语并返回集合类型数据的消息.更进一步说: 对于集合类型的主语,coollectionKey的数量总是根据主语的集合内的元素数量, coollectionKey 本身并无实际意义.
示例:
NSArray *array = [NSArray arrayWithObject:@"foo"];
[[array should] have:1] item];
Car *car = [Car car];
[car setPassengers:[NSArray arrayWithObjects:@"Eric", "Stan", nil]];
[[[[car passengers] should] haveAtLeast:2] items];
[[[car should] haveAtLeast:2] passengers];
这些期望用于验证主语是否在从创建期望到用例结束的这段时间里接收到了某个消息(或者说对象的某个方法是否被调用).这个期望会同时存储 选择器或参数等信息,并依次来决定期望是否满足。这些期望可用于真实或模拟的独享,但是在设置 receive 表达式时,Xcode 可能会给警告(报黄).
对参数无要求的选择器:
[[subject should] receive:(SEL)aSelector]
[[subject should] receive:(SEL)aSelector withCount:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector withCountAtLeast:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector withCountAtMost:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast:(NSUInteger)aCount]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost:(NSUInteger)aCount]
含有指定参数的选择器:
[[subject should] receive:(SEL)aSelector withArguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCount:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCountAtLeast:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector withCountAtMost:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withArguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast:(NSUInteger)aCount arguments:(id)firstArgument, ...]
[[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost:(NSUInteger)aCount arguments:(id)firstArgument, ...]
示例:
subject = [Cruiser cruiser];
[[subject should] receive:@selector(energyLevelInWarpCore:)
andReturn:theValue(42.0f) withCount:2 arguments:theValue(7)];
[subject energyLevelInWarpCore:7];
float energyLevel = [subject energyLevelInWarpCore:7];
[[theValue(energyLevel) should] equal:theValue(42.0f)];
注意你可以将 any() 通配符用作参数.如果你只关心一个方法的部分参数的值,这回很有用:
id subject = [Robot robot];
[[subject should] receive:@selector(speak:afterDelay:whenDone:) withArguments:@"Hello world",any(),any()];
[subject speak:@"Hello world" afterDelay:3 whenDone:nil];
[[@"MyNotification" should] bePosted];
[[@"MyNotification" should] bePostedWithObject:(id)object];
[[@"MyNotification" should] bePostedWithUserInfo:(NSDictionary *)userInfo];
[[@"MyNotification" should] bePostedWithObject:(id)object andUserInfo:(NSDictionary *)userInfo];
[[@"MyNotification" should] bePostedEvaluatingBlock:^(NSNotification *note)block];
示例:
it(@"Notification", ^{ // 期望:通知
[[@"自定义通知" should] bePosted];
NSNotification *myNotification = [NSNotification notificationWithName:@"自定义通知"
object:nil];
[[NSNotificationCenter defaultCenter] postNotification:myNotification];
});
[[subject shouldEventually] receive:(SEL)aSelector]
[[subject shouldEventually] receive:(SEL)aSelector withArguments:(id)firstArgument, ...]
[[theBlock(^{ ... }) should] raise]
[[theBlock(^{ ... }) should] raiseWithName:]
[[theBlock(^{ ... }) should] raiseWithReason:(NSString *)aReason]
[[theBlock(^{ ... }) should] raiseWithName:(NSString *)aName reason:(NSString *)aReason]
示例:
[[theBlock(^{
[NSException raise:@"FooException" reason:@"Bar-ed"];
}) should] raiseWithName:@"FooException" reason:@"Bar-ed"];
Kiwi中,自定义匹配器的最简单方式是创建KWMatcher的子类,并以适当的方式重写下面示例中的方法.为了让你自定义的匹配器在规则中可用,你需要在规则中使用 registerMatchers(namespacePrefix)进行注册.看下Kiwi源文件中的匹配器写法(如KWEqualMatcher等),将会使你受益匪浅.
registerMatchers 待补充
模拟对象模拟某个类,或者遵循某个写一个.他们让你在完全功能完全实现之前,就能更好地专注于对象间的交互行为,并且能降低对象间的依赖--模拟或比避免那些运行规则时几乎很难出现的情况.
it(@"Mock", ^{ // 模拟对象
id carMock = [ZJHKiwiCar mock]; // 模拟创建一个对象
[ [carMock should] beMemberOfClass:[ZJHKiwiCar class]]; // 判断对象的类型
[ [carMock should] receive:@selector(currentGear) andReturn:theValue(3)];
[ [theValue([carMock currentGear]) should] equal:theValue(3)]; // 调用模拟对象的方法
id carNullMock = [ZJHKiwiCar nullMock]; // 模拟创建一个空对象
[ [theValue([carNullMock currentGear]) should] equal:theValue(0)];
[carNullMock applyBrakes];
// 模拟协议
id flyerMock = [KWMock mockForProtocol:@protocol(ZJHKiwiFlyingMachine)];
[ [flyerMock should] conformToProtocol:@protocol(ZJHKiwiFlyingMachine)];
[flyerMock stub:@selector(dragCoefficient) andReturn:theValue(17.0f)];
id flyerNullMock = [KWMock nullMockForProtocol:@protocol(ZJHKiwiFlyingMachine)];
[flyerNullMock takeOff];
});
通常模拟对象收到一个非预期的选择器或消息模式时,会抛出异常(PS:iOS开发常见错误奔溃之一).在模拟对象上使用 stub 或 receive期望,期望的消息会自动添加到模拟对象上,以实现对方法的模拟。如果你不关心模拟对象如何处理其他非预期的消息,也不想在收到非预期消息时抛出异常,那就使用 null 模拟对象吧(也即 null 对象).
当mock对象收到了没有被stub过的调用(更准确的说,走进了消息转发的forwoardInvocation:方法里)时:
创建类的模拟实例(NSObject 扩展):
[SomeClass mock]
[SomeClass mockWithName:(NSString *)aName]
[SomeClass nullMock]
[SomeClass nullMockWithName:(NSString *)aName]
创建类的模拟实例:
[KWMock mockForClass:(Class)aClass]
[KWMock mockWithName:(NSString *)aName forClass:(Class)aClass]
[KWMock nullMockForClass:(Class)aClass]
[KWMock nullMockWithName:(NSString *)aName forClass:(Class)aClass]
创建遵循某协议的实例:
[KWMock mockForProtocol:(Protocol *)aProtocol]
[KWMock mockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
[KWMock nullMockForProtocol:(Protocol *)aProtocol]
[KWMock nullMockWithName:(NSString *)aName forProtocol:(Protocol *)aProtocol]
存根,能返回指定定选择器或消息模式的封装好的请求.Kiwi中,你可以存根真实对象(包括类对象)或模拟对象的方法.没有指定返回值的存根,将会对应返回nil,0等零值.存根需要返回标量的,标量需要使用 theValue(某个标量)宏 装箱。所有的存根都会在规范的一个例子的末尾(一个itblock)被清除.
存根选择器:
[subject stub:(SEL)aSelector]
[subject stub:(SEL)aSelector andReturn:(id)aValue]
存根消息模式:
[ [subject stub] *messagePattern*]
[ [subject stubAndReturn:(id)aValue] *messagePattern*]
示例:
it(@"stub", ^{ // 存根
id mock = [ZJHKiwiCar mock]; // 设置对象的名字为Rolls-Royce
[mock stub:@selector(carName) andReturn:@"Rolls-Royce"];
[ [[mock carName] should] equal:@"Rolls-Royce"];
// 模拟对象接收的消息的某个参数是一个block;通常必须捕捉并执行这个block才能确认这个block的行为.
id robotMock = [KWMock nullMockForClass:[ZJHKiwiCar class]];
// 捕捉block参数
KWCaptureSpy *spy = [robotMock captureArgument:@selector(speak:afterDelay:whenDone:) atIndex:2];
// 设置存储参数
[[robotMock should] receive:@selector(speak:) withArguments:@"Goodbye"];
// 模拟对象接收的消息的某个参数是一个block
[robotMock speak:@"Hello" afterDelay:2 whenDone:^{
[robotMock speak:@"Goodbye"];
}];
// 执行block参数
void (^block)(void) = spy.argument;
block();
});
有时,你可能想要捕捉传递给模拟对象的参数.比如,参数可能没有是一个没有很好实现 isEqual: 的对象,如果你想确认传入的参数是否是需要的,那就要单独根据某种自定义规则去验证.另外一种情况,也是最常遇到的情况,就是模拟对象接收的消息的某个参数是一个block;通常必须捕捉并执行这个block才能确认这个block的行为。示例如上
未来的某天,你或许需要存根alloc等方法.这可能不是一个好主意,但是如果你坚持,Kiwi也是支持的.需要提前指出的是,这么做需要深入思考某些细节问题,比如如何管理初始化。Kiwi 存根遵循 Objective-C 的内存管理机制.当存根将返回值写入一个对象时,如果选择器是以alloc,或new开头,或含有 copy时,retain消息将会由存根自动在对象发送前发送。因此,调用者不需要特别处理由存根返回的对象的内存管理问题.
Kiwi深度依赖Objective-C的运行时机制,包括消息转发(比如 forwardInvocation:).因为Kiwi需要预先判断出来哪些方法可以安全调用.使用Kiwi时,有一些惯例,也是你需要遵守的。为了使情况简化和有条理,某些方法/选择器,是决不能在消息模式中使用,接收期望,或者被存根;否则它们的常规行为将会被改变.不支持使用这些控制器,而且使用后的代码的行为结果也会变的很奇怪。在实践中,对于高质量的程序代码,你可能不需要担心这些,但是最好还是对这些有些印象
黑名单(使用有风险):
-class, -superclass, -retain, -release等.)白名单(可安全使用):
+alloc+new+copy-copy-mutableCopy-isEqual:-description-hash-initiOS应用经常有组件需要在后台和主线程中内容沟通.为此,Kiwi支持异步测试;因此就可以进行集成测试-一起测试多个对象.
为了设置异步测试,你 必须 使用 expectFutureValue 装箱,并且使用 shouldEventually 或 shouldEventuallyBeforeTimingOutAfter来验证。shouldEventually 默认在判定为失败前等待一秒.
[[expectFutureValue(myObject) shouldEventually] beNonNil];
标量的处理:当主语中含有标量时,应该使用 expectFutureValue中使用 theValue装箱标量,例如:
[[expectFutureValue(theValue(myBool)) shouldEventually] beYes];
shouldEventuallyBeforeTimingOutAfter():这个block默认值是2秒而不是1秒.
[[expectFutureValue(fetchedData) shouldEventuallyBeforeTimingOutAfter(2.0)] equal:@"expected response data"];
也有shouldNotEventually和 shouldNotEventuallyBeforeTimingOutAfter 的变体.
这个block会在匹配器满足或者超时(默认: 1秒)时完成。This will block until the matcher is satisfied or it times out (default: 1s)
it(@"shouldEventually", ^{ // 异步测试
__block NSString *featchData = nil;
// 模拟发送请求,处理异步回调
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
featchData = @"数据返回";
});
[[expectFutureValue(featchData) shouldEventually] beNonNil];
});
完成代码可下载:ZJHUnitTestDemo
ArrayDataSource:
typedef void (^TableViewCellConfigureBlock)(id cell, id item);
@interface ArrayDataSource : NSObject <UITableViewDataSource>
- (id)initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;
- (id)itemAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface ArrayDataSource ()
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, copy) TableViewCellConfigureBlock configureCellBlock;
@end
@implementation ArrayDataSource
- (id)init {
return nil;
}
- (id)initWithItems:(NSArray *)anItems cellIdentifier:(NSString *)aCellIdentifier configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock {
self = [super init];
if (self) {
self.items = anItems;
self.cellIdentifier = aCellIdentifier;
self.configureCellBlock = [aConfigureCellBlock copy];
}
return self;
}
- (id)itemAtIndexPath:(NSIndexPath *)indexPath {
return self.items[(NSUInteger) indexPath.row];
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
self.configureCellBlock(cell, item);
return cell;
}
@end
PhotosViewController
static NSString * const PhotoCellIdentifier = @"PhotoCell";
@interface PhotosViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) ArrayDataSource *photosArrayDataSource;
@end
@implementation PhotosViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"Photos";
[self setupTableView];
}
- (void)setupTableView {
TableViewCellConfigureBlock configureCell = ^(PhotoCell *cell, Photo *photo) {
[cell configureForPhoto:photo];
};
Store *st =[Store sharedInstance];
NSArray *photos = [st sortedPhotos];
self.photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
cellIdentifier:PhotoCellIdentifier
configureCellBlock:configureCell];
self.tableView.dataSource = self.photosArrayDataSource;
[self.tableView registerClass:[PhotoCell class] forCellReuseIdentifier:PhotoCellIdentifier];
}
#pragma mark UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
PhotoViewController *photoViewController = [[PhotoViewController alloc] init];
photoViewController.photo = [self.photosArrayDataSource itemAtIndexPath:indexPath];
[self.navigationController pushViewController:photoViewController animated:YES];
}
@end
ArrayDataSourceSpec是针对ArrayDataSource的测试用例,基本思路是我们希望在为一个 tableView 设置好数据源后,tableView 可以正确地从数据源获取组织 UI 所需要的信息,基本上来说,也就是能够得到“有多少行”以及“每行的 cell 是什么”这两个问题的答案。到这里,有写过 iOS 的开发者应该都明白我们要测试的是什么了。没错,就是 -tableView:numberOfRowsInSection: 以及 -tableView:cellForRowAtIndexPath: 这两个接口的实现。我们要测试的是 ArrayDataSource 类,因此我们生成一个实例对象。在测试中我们不希望测试依赖于 UITableView,因此我们 mock 了一个对象代替之。接下来向 dataSource 发送询问元素个数的方法,这里应该毫无疑问返回数组中的元素数量。接下来我们给 mockTableView 设定了一个期望,当将向这个 mock 的 tableView 请求 dequeu indexPath 为 (0,0) 的 cell 时,将直接返回我们预先生成的一个 cell,并进行接下来的处理。完成设定后,我们调用要测试的方法 [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath]。dataSource 在接到这个方法后,向 mockTableView 请求一个 cell(这个方法已经被 mock),接下来通过之前定义的 block 来对 cell 进行配置,最后返回并赋值给 result。于是,我们就得到了一个可以进行期望断言的 result,它应该和我们之前做的 cell 是同一个对象,并且经过了正确的配置。至此这个 dataSource 测试完毕。
describe(@"ArrayDataSource", ^{
// init方法校验
context(@"Initializing", ^{
it(@"should not be allowed using init", ^{
[[[[ArrayDataSource alloc] init] should] beNil];
});
});
// 配置方法校验
context(@"Configuration", ^{
__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
[[configuredObject should] equal:@"a"];
};
// 生成数据源
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];
// mock一个tableView
id mockTableView = [UITableView mock];
UITableViewCell *cell = [[UITableViewCell alloc] init];
__block id result = nil;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
it(@"should receive cell request", ^{
// tableView设置存根
[[mockTableView should] receive:@selector(dequeueReusableCellWithIdentifier:forIndexPath:)
andReturn:cell
withArguments:@"foo",indexPath];
// dataSource 调用代理方法
result = [dataSource tableView:mockTableView cellForRowAtIndexPath:indexPath];
});
it(@"should return the dummy cell", ^{
[[result should] equal:cell];
});
});
// 获取数据方法校验
context(@"number of rows", ^{
id mockTableView = [UITableView mock];
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:nil];
it(@"should be 2 items", ^{
NSInteger count = [dataSource tableView:mockTableView numberOfRowsInSection:0];
[[theValue(count) should] equal:theValue(2)];
});
});
});
SPEC_END
PhotosViewControllerSpec是针对PhotosViewController的测试用例。我们模拟了 tableView 中对一个 cell 的点击,然后检查 navigationController 的 push 操作是否确实被调用,以及被 push 的对象是否是我们想要的下一个 ViewController。要测试的是 PhotosViewController 的实例,因此我们生成一个。对于它的 UINavigationController,因为其没有在导航栈中,也这不是我们要测试的对象(保持测试的单一性),所以用一个 mock 对象来代替。然后为其设定 -pushViewController:animated: 需要被调用的期望。然后再用输入参数捕获将被 push 的对象抓出来,进行判断。在这里我们用 stub 替换了 photosViewController 的 navigationController,这个替换进去的 UINavigationController 的 mock 被期望响应 -pushViewController:animated:。于是在点击 tableView 的 cell 时,我们期望 push 一个新的 PhotoViewController 实例,这一点可以通过捕获 push 消息的参数来达成。关于 mock 还有一点需要补充的是,使用 +mock 方法生成的 mock 对象对于期望收到的方法是严格判定的,就是说它能且只能响应那些你添加了期望或者 stub 的方法。比如只为一个 mock 设定了 should receive selector(a) 这样的期望,那么对这个 mock 发送一个消息 b 的话,将会抛出异常 (当然,如果你没有向其发送消息 a 的话,测试会失败)。如果你的 mock 还需要相应其他方法的话,可以使用 +nullMock 方法来生成一个可以接受任意预定消息而不会抛出异常的空 mock。
describe(@"PhotosViewController", ^{
context(@"when click a cell in table view", ^{
it(@"A PhotoViewController should be pushed", ^{
// 新建PhotosViewController对象
PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
// 判断view的创建
UIView *view = photosViewController.view;
[[view shouldNot] beNil];
// mock一个导航条
UINavigationController *mockNavController = [UINavigationController mock];
// 设置photosViewController存根
[photosViewController stub:@selector(navigationController) andReturn:mockNavController];
// 设置mockNavController存根
[[mockNavController should] receive:@selector(pushViewController:animated:)];
// 添加参数捕捉
KWCaptureSpy *spy = [mockNavController captureArgument:@selector(pushViewController:animated:)
atIndex:0];
// 调用参数
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
// 获取捕捉的参数
id obj = spy.argument;
PhotoViewController *vc = obj;
// 校验参数是否正确
[[vc should] beKindOfClass:[PhotoViewController class]];
[[vc.photo shouldNot] beNil];
});
});
});
SPEC_END
以章节 2.1、Kiwi测试的基本结构 示例为例,最开头的SPEC_BEGIN(SimpleStringSpec)和结尾的SPEC_END。这是两个宏,我们来看看它们的定义:
// Example group declarations.
#define SPEC_BEGIN(name) \
\
@interface name : KWSpec \
\
@end \
\
@implementation name \
\
+ (NSString *)file { return @__FILE__; } \
\
+ (void)buildExampleGroups { \
[super buildExampleGroups]; \
\
id _kw_test_case_class = self; \
{ \
/* The shadow `self` must be declared inside a new scope to avoid compiler warnings. */ \
/* The receiving class object delegates unrecognized selectors to the current example. */ \
__unused name *self = _kw_test_case_class;
#define SPEC_END \
} \
} \
\
@end
通过这段定义我们知道了两件事:
SimpleStringSpec类是KWSpec的子类,重写了一个叫buildExampleGroups的方法buildExampleGroups的方法体里的实际上,KWSpec作为XCTextCase的子类,重写了+ (NSArray *)testInvocations方法以返回所有测试用例对应的Invocation。在执行这个方法的过程中,会使用KWExampleSuiteBuilder构建Spec树。KWExampleSuiteBuilder会先创建一个根节点,然后调用我们的buildExampleGroups方法,以DFS的方式构建Spec树。当前的结点路径记录在KWExampleSuiteBuilder单例的contextNodeStack中,栈顶元素就是此时的context结点。
在每个结点里,都有一个KWCallSite的字段,里面有两个属性:fileName和lineNumber,用于在测试失败时精确指出问题出现在哪一行,这很重要。这些信息是在运行时通过
atos命令获取的。如果你感兴趣,可以在 KWSymbolicator.m 中看到具体的实现
这样就很容易理解我们写的Spec本质上是什么了:context(...)是调用一个叫context的C函数,将当前context结点入栈,并加到上层context的子节点列表中,然后调用block()。let(...)宏展开后是声明一个变量,并调用let_函数将一个let结点加到当前context的letNodes列表里。其他节点的行为也都大致相同。这里特别说明一下it和pending,除了把自己添加到当前的context里之外,还会创建一个KWExample,后者是一个用例的抽象。它会被加到一个列表中,用于后续执行测试时调用。
在buildExampleGroups方法中,Kiwi构建了内部的Spec树,根节点记录在KWExampleSuite对象里,后者被存储在KWExampleSuiteBuilder的一个数组中。此外,在构建过程中遇到的所有it结点和pending结点,也都各自生成了KWExample对象,按照正确的顺序加入到了KWExampleSuite对象中。万事俱备。现在只需要返回所有test case对应的Invocation,后面就交给系统框架去调用啦。
这些invocation的IMP是
KWSpec对象里的runExample方法。但Kiwi为了给方法一个更有意义的名字,在运行时创建了新的selector,这个新selector根据当前Spec以及context的description,用驼峰命名组合而成的。虽然此举是出于提高可读性的考虑,但实际上组合出来的名字总是非常冗长,读起来很困难。
就在刚刚,Kiwi已经构建出了一个清晰漂亮的Spec Tree,并把所有用例抽象成一个个KWExample,在testInvocations方法中返回了它们对应的Invocation。现在一切已经准备妥当,系统组件要开始调用Kiwi返回的Invocation了。之前我们说了,这些Invocation的实现是runExample,它会做什么呢?
我们只讨论it结点。因为pending结点实际上并不会做什么实质性的事情。经过层层调用,首先会进入KWExample的visitItNode:方法里。这个方法将以下所有操作包装进一个block里(我们叫它block1):
it block里的代码——你的部分用例在这一步就已经完成了检查verifiers进行自检——这就是检查你另一部分用例是否通过的时机。后面我们还会详细说明expectation没有被满足,报告用例失败,否则报告通过spy和stub (不影响mock对象)。 这意味着如果你希望在整个用例里都执行某个stub或spy,那么你最好把它写进beforeEach里Mock
我们来介绍一下Kiwi中生成一个Mock的方法:
+ (id)mock; 来mock某个类[KWMock mockForProtocol:] 来生成一个遵循了某协议的对象[KWMock partialMockForObject:] 来根据已有object生成一个mock了该object类型的对象KWMock还提供了nullMockFor...方法。与上面方法的不同在于:当mock对象收到了没有被stub过的调用(更准确的说,走进了消息转发的forwoardInvocation:方法里)时:
现在假设我们以[ZJHNetworkTool mock]方法生成了一个KWMock对象,来看看这个有用的功能是怎么实现的
Stub a Method
下面介绍了你在stub一个mock对象时时,可能会用到的参数:
theValue()函数包装它,而不是用@()指令。(theValue(0.8)√ / @(0.8)×)当你调用了[networkMock stub:@selector(requestUrl:param:completion:) withBlock:^id(NSArray *params){..}];
KWMock将会:
KWMessagePattern,后者是KWStub中用于唯一区分方法的数据结构(而不是用selector)KWMessagePattern生成一个KWStub对象。如果你在初始化KWMock时指定了block、returnValue、argument filter等信息,也会一并传给KWStub
KWStub他放到自身的列表里现在你已经成功stub了一个mock对象中的方法。现在你调用 [networkMock requestUrl:@"someURL" param:@{} completion:^(NSDictionary *respondDic) { }]时,由于KWMock对象本身没有实现这个方法,将不会真正的走到HYNetworkEngine的下载逻辑里,而是执行所谓完全消息转发。KWMock重写了那两个方法。其中:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector返回自己mock的Class或Protocol对此selector的methodSignature。如果找不到,就用默认的"v@:"构造一个返回(还认识它吧?)
接下来进入了 - (void)forwardInvocation:(NSInvocation *)anInvocation方法:
expect...接口向这里添加messagePatternnil都被替换为了[NSNull null])。然后将返回值写进invocation。最后返回YES,结束责任链消息转发处理的代码如下,至此,我们向mock对象创建和调用stub方法的步骤都已经完成了
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 将本次调用通知给关心它的spies
for (KWMessagePattern *messagePattern in self.messageSpies) {
if ([messagePattern matchesInvocation:invocation]) {
NSArray *spies = [self.messageSpies objectForKey:messagePattern];
for (id<KWMessageSpying> spy in spies) {
[spy object:self didReceiveInvocation:invocation];
}
}
}
for (KWStub *stub in self.stubs) {
if ([stub processInvocation:invocation])
return;
}
if (self.isPartialMock)
[anInvocation invokeWithTarget:self.mockedObject];
if (self.isNullMock)
return;
// expectedMessagePattern除了所有被stub的方法外
// 还包括KWMock从NSObject中继承的白名单方法方法,如description、hash等
for (KWMessagePattern *expectedMessagePattern in self.expectedMessagePatterns) {
if ([expectedMessagePattern matchesInvocation:anInvocation])
return;
}
[NSException raise:@"KWMockException" format:@"description"];
}
当我们写下should、shouldEventually、beNil、graterThan、receive等语句时,Kiwi为我们做了什么?延时判断是怎么实现的?前面说的registerMatchers语句有什么用?接下来我们会一一分析。
Kiwi中对
Expectation的理解是:一个对象(称它为 subject)在现在或将来的某个时候 应该(should) 或 不应该(shouldNot) 满足某个条件。
在Kiwi中,有一个概念叫Verifier,顾名思义,是用于判断 subject 是否满足某个条件的。Verifier在Kiwi中共分为三种,分别是:
[subject shouBeNil]
[[subject should] beNil]
MatcherVerifier的子类。不同的是,它用来执行延时判断。对应的调用方式包括 如果你在用AsyncVerifier,别忘了用expectFutureValue函数包装你的 subject,以便在它的值改变时,Kiwi依然能够找到它。[[expectFutureValue(subject) shouldEventuallyBeforeTimingOutAfter(0.5)] beNil]、[[expectFutureValue(subject) shouldAfterWaitOf(0.5)] beNil]
MatchVerifier
假设我们有这样的一个Expectation:
[[resultError should] equal:[NSNull null]];
这段代码中,should实际上是一个宏,它创建了一个MatchVerifier,把它添加到当前Example的 verifiers 列表里,并返回这个MatchVerifier。接下来,我们调用了equal方法。实际上,MatchVerifier并没有实现这个方法,因此会走进转发逻辑。在forwardInvocation:方法中,MatchVerifier会从 matcherFactory 中查找实现了equal方法的Matcher。后者是一个遵循KWMatching协议的对象,用来判断 subject 是否满足某个条件。matcherFactory 最终找到了一个Kiwi中内置的,叫KWEqualMatcher的类,它实现了equal方法,并且没有在自己的canMatchSubject:方法中返回 NO。因此,MatchVerifier会将消息转发给它的实例。
之后,MatchVerifier会根据 matcher 的shouldBeEvaluatedAtEndOfExample方法返回值,来决定立刻调用 matcher 中实现的evaluate方法来检测测试结果,还是等到整个 Example 执行完成后(也就是说,你在这个it节点内写的代码都执行之后。还记得前面执行测试用例那一小节提到的 verifiers 自检步骤吗?)才检查。
Kiwi内置的 matcher 中,只有
KWNotificationMatcher和KWReceiveMatcher是在 Example 执行完成后进行检查的,其余都是立即检查
registerMatchers
现在我们已经知道 matcherFactory 注册和使用 matcher 的原理了,自定义一个 matcher 也是水到渠成的事情。事实上,我们只需要创建一个遵循KWMatching协议的类——当然,继承KWMatcher或许是一个更方便的选择。这个类中需要实现的方法和其作用,我们大部分都已经说过了。接下来,在当前的 context 下使用registerMatchers函数将你的 matcher 注册给 matcherFactory,记得传入的参数要和你刚刚创建的 matcher 类名前缀严格一致。
AsyncVerifier
上面说过,AsyncVerifier是MatchVerifier的子类。这意味着,它也是通过 matcherFactory 提供的 matcher 去判断你的 Expectation 是否通过的。唯一不同的是,它会以0.1s为周期对结果进行轮询。具体的实现方式为:在当前线程使用 Default 模式,以0.1s为时长运行RunLoop。这意味着,虽然它的名字带了Async,但实际上它的轮询操作是同步执行的。你最好把AsyncVerifier这个名字理解为:用于测试你的Async操作结果的Verifyer。
所以,一般情况下没有必要把等待时间设置得过长。
AsyncVerifier有两种使用方法,分别是shouldEventually...和shouldAfterWait...,你可以指定等待的时间,否则默认为1秒。两种方法的区别在于:前者在轮询过程中发现预期的结果已经满足,会立刻返回。后者则会固定执行到给定的等待时间结束后才检测结果。
参考链接:
TDD的iOS开发初步以及Kiwi使用入门:https://onevcat.com/2014/02/ios-test-with-kiwi/
Kiwi,BDD行为测试框架--iOS攻城狮进阶必备技能:https://cloud.tencent.com/developer/article/1011286
iOS 自动化测试框架 Kiwi 的使用介绍及原理分析:https://cloud.tencent.com/developer/article/1972234
Kiwi 使用进阶 Mock, Stub, 参数捕获和异步测试:https://onevcat.com/2014/05/kiwi-mock-stub-test/
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po