【CodeTest】Cedar介绍
学习资料
Cedar介绍
Cedar是OC开发中,BBD风格的一个主流单元测试框架,关于BBD的介绍可以参考这篇文章.
CocoaPods安装Cedar
target 'MyAppTests' do
pod 'Cedar'
end
可以利用Alcatraz安装Cedar测试文件模板.
语法简介
#import <Cedar/Cedar.h>
#import "NumberSequencer.h"
using namespace Cedar::Matchers;
using namespace Cedar::Doubles;
SPEC_BEGIN(NumberSequencerSpec)
/* 就像前两篇博客所写的Quick一样,Cedar支持集中测试和屏蔽测试,同理的,是在测试方法前加 f 和 x,事实上,swift下的Quick,肯定是得到OC单元测试框架启发的 */
/* beforeEach 相当于 setup */
describe(@"NumberSequencer", ^{
__block NumberSequencer *myNumberSequencer;
beforeEach(^{
myNumberSequencer = [NumberSequencer new];
});
it(@"nextAfter: returns the next integer greater than the argument", ^{
[myNumberSequencer nextAfter:2] should equal(3);
});
});
/* subjectAction 用于描述一个 top-level 的方法或事件,它和beforeEach的区别在于,在一个作用域里,它只能有一个,而且,它在所有beforeEach之后执行 */
describe(@"thing", ^{
__block BOOL parameter;
// subjectAction(^{ [object doThingWithParameter:parameter]; });
describe(@"when something is true", ^{
beforeEach(^{
parameter = YES;
});
it(@"should ...", ^{
// ...
});
});
});
/* context 是 describe 的别名,用于描述不同状态或环境 */
describe(@"NumberSequencer", ^{
__block NumberSequencer *myNumberSequencer;
context(@"when created with the default constructor", ^{
beforeEach(^{
myNumberSequencer = [NumberSequencer new];
});
it(@"nextAfter: returns the next integer greater than the argument", ^{
[myNumberSequencer nextAfter:2] should equal(3);
});
it(@"previousBefore:returns the largest number less than the argument", ^{
[myNumberSequencer previousBefore:2] should equal(0);
});
context(@"when constructed with an interval", ^{
beforeEach(^{
myNumberSequencer = [[NumberSequencer alloc] initWithInterval:2];
});
it(@"nextAfter: returns the sum of the argument and the interval", ^{
[myNumberSequencer nextAfter:2] should equal(4);
});
it(@"previousBefore: returns the difference between the argument and the interval", ^{
[myNumberSequencer previousBefore:2] should equal(-1);
});
});
});
});
/* +beforeEach 和 +afterEach 相当于全局 beforeEach 和 afterEach, 它们先于所有spec前执行 */
/* Cedar 支持 shared example groups */
sharedExamplesFor(@"a similarly-behaving thing", ^(NSDictionary *sharedContext) {
it(@"should do something common", ^{
//...
});
});
describe(@"Something that shares behavior", ^{
itShouldBehaveLike(@"a similarly-behaving thing");
});
describe(@"Something else that shares behavior", ^{
itShouldBehaveLike(@"a similarly-behaving thing");
});
sharedExamplesFor(@"a red thing", ^(NSDictionary *sharedContext) {
it(@"should be red", ^{
// Thing *thing = [sharedContext objectForKey:@"thing"];
// expect(thing.color).to(equal(red));
});
});
describe(@"A fire truck", ^{
beforeEach(^{
// [[SpecHelper specHelper].sharedExampleContext setObject:[FireTruck fireTruck] forKey:@"thing"];
});
itShouldBehaveLike(@"a red thing");
});
describe(@"An apple", ^{
beforeEach(^{
// [[SpecHelper specHelper].sharedExampleContext setObject:[Apple apple] forKey:@"thing"];
});
itShouldBehaveLike(@"a red thing");
});
SPEC_END
Double语法
Double提供了BBD中的核心功能,stub 和 mock,关于它们的讨论,参见置换测试: Mock, Stub 和其他
我摘录其中的一些观点,便于理解:
double 可以理解为置换,它是所有模拟测试对象的统称,我们也可以称它为替身。一般来说,当你创建任意一种测试置换对象时,它将被用来替代某个指定类的对象。
stub 可以理解为测试桩,它能实现当特定的方法被调用时,返回一个指定的模拟值。如果你的测试用例需要一个伴生对象来提供一些数据,可以使用 stub 来取代数据源,在测试设置时可以指定返回每次一致的模拟数据。
spy 可以理解为侦查,它负责汇报情况,持续追踪什么方法被调用了,以及调用过程中传递了哪些参数。你能用它来实现测试断言,比如一个特定的方法是否被调用或者是否使用正确的参数调用。当你需要测试两个对象间的某些协议或者关系时会非常有用。
mock 与 spy 类似,但在使用上有些许不同。spy 追踪所有的方法调用,并在事后让你写断言,而 mock 通常需要你事先设定期望。你告诉它你期望发生什么,然后执行测试代码并验证最后的结果与事先定义的期望是否一致。
fake 是一个具备完整功能实现和行为的对象,行为上来说它和这个类型的真实对象上一样,但不同于它所模拟的类,它使测试变得更加容易。一个典型的例子是使用内存中的数据库来生成一个数据持久化对象,而不是去访问一个真正的生产环境的数据库。
实践中,这些术语常常用起来不同于它们的定义,甚至可以互换。稍后我们在这篇文章中会看到一些库,它们自认为自己是 "mock 对象框架",但是其实它们也提供 stub 的功能,而且验证行为的方式也类似于我描述的 "spy" 而不是 "mock"。所以不要太过于陷入这些词汇的细节;我下这些定义更多的是因为要在高层次上区分这些概念,并且它对考虑不同类型测试对象的行为会有帮助。
另外,我从王巍大神的博客Kiwi 使用进阶 Mock, Stub, 参数捕获和异步测试深受启发.虽然我没有使用Kiwi(我没有使用Kiwi,而使用Cedar,是个阴差阳错的巧合),但是单元测试中的思想,甚至语法都是相同的,可以相互借鉴.
说一些我的体会,简单来说,stub是用来伪造一个方法,阻断对原来方法的调用,而mock是用来模拟一个类,或模拟一个遵循了某些协议的对象.stub和mock都是为了隔绝测试中的对象,保证测试中,变量的单一性(在我们上学做实验时,一定知道'控制变量法',比如,我们测试某种酶的活性随温度变化的实验,我们肯定要保证其他变量不变,比如湿度要保持不变.同理,我们要测试控制器中TableView的初始化方法,那么我们就要保证TableView的数据源不变).
接下来,介绍一些API
spy_on(someInstance);
如果我们spy_on某个对象,当这个对象的方法被stub了,spy_on会获得相应的信息,如果方法没有stub,那么会调用对象的真正方法.
// class fakes
id<CedarDouble> fake = fake_for(someClass);
id<CedarDouble> niceFake = nice_fake_for(someClass);
// protocol fakes
id<CedarDouble> anotherFake = fake_for(@protocol(someProtocol));
id<CedarDouble> anotherNiceFake = nice_fake_for(@protocol(someProtocol));
fake相当于mock,我们fake一个对象,如果该对象调用了没有被stub的方法,这个方法会返回0/nil/NULL
.fake 和nice_fake的区别在于,fake的对象,调用没有被stub的方法,会抛出异常,而nice_fake的对象,则会继续保持调用.这对应于Kiwi中的mock和nullMock.
//stubbing all calls to method:; "method:" can be used instead of @selector("method:") for brevity
fake stub_method(@selector("method:"));
fake stub_method("method:");
//only stubbing calls with specific arguments
fake stub_method("method:").with(x);
//methods with multiple arguments; both forms below are equivalent
fake stub_method("method:withSecondArg:").with(x).and_with(y);
fake stub_method("method:withSecondArg:").with(x, y);
//matching an arbitrary argument
fake stub_method("method:withSecondArg:").with(x, Arguments::anything);
//matching an arbitrary instance of a specific class
fake stub_method("method:withSecondArg:").with(x, Arguments::any([NSArray class]));
//return a canned value:
fake stub_method("method:").and_return(z);
fake stub_method("method:").with(x).and_return(z);
//execute an alternative implementation provided by your test:
fake stub_method("method").and_do(^(NSInvocation * invocation) {
//do something different here
});
//raise an exception:
fake stub_method("method").and_raise_exception();
fake stub_method("method").and_raise_exception([NSException]);
以上是stub的一些API,包括带参数的,带返回值的,抛出异常的.
[(id<CedarDouble>)spy reset_sent_messages];
NSArray *messages = [(id<CedarDouble>)spy sent_messages];
NSArray *someMethodMessages = [(id<CedarDouble>)spy sent_messages_with_selector:@selector(someMethod:)];
以上利用sent_messages
捕获调用,利用sent_messages_with_selector:
捕获特定调用,利用reset_sent_messages
重置调用.
最后,我们写两个测试来说明.
�第一个测试,我们仿照行为驱动开发举例说明中的第一个例子,消息格式化EventDescriptionFormatter写一个测试.
源码:
#import <Cedar/Cedar.h>
#import "EventDescriptionFormatter.h"
#import "NSDate+StringFormatter.h"
#import "Event.h"
using namespace Cedar::Matchers;
using namespace Cedar::Doubles;
SPEC_BEGIN(EventDescriptionFormatterSpec)
describe(@"EventDescriptionFormatter", ^{
__block EventDescriptionFormatter * desFormatter;
__block NSString * description;
__block id<CedarDouble,Event> fakeEvent;
beforeEach(^{
NSDate * startDate = [NSDate dateFromString:@"2015-11-27 09:52:00"];
NSDate * endDate = [NSDate dateFromString:@"2015-11-27 10:52:00"];
fakeEvent = nice_fake_for(@protocol(Event));
fakeEvent stub_method("name").and_return(@"Fixture Time");
fakeEvent stub_method("startDate").and_return(startDate);
fakeEvent stub_method("endDate").and_return(endDate);
desFormatter = [EventDescriptionFormatter new];
description = [desFormatter eventDescriptionFromEvent:fakeEvent];
});
it(@"should return formatted description", ^{
expect(description).to(equal(@"Fixture Time:开始于2015-11-27 09:52:00,结束于2015-11-27 10:52:00"));
});
// 利用sent_messages捕获调用
it(@"sent messages", ^{
NSArray *messages = [fakeEvent sent_messages];
messages.count should equal(3);
NSLog(@"messages = %@",messages);
// 捕获第一个调用
NSInvocation *firstInvocation = messages.firstObject;
firstInvocation.selector should equal(@selector(name));
NSLog(@"firstInvocation = %@",firstInvocation);
// 特定捕获
NSArray *messageWithSelector = [fakeEvent sent_messages_with_selector:@selector(name)];
messageWithSelector.count should equal(1);
NSLog(@"messageWithSelector = %@",messageWithSelector);
});
// PENDING 用来 TODO
it(@"test pending", PENDING);
});
SPEC_END
我们要测试EventDescriptionFormatter
这个类的一个实例方法eventDescriptionFromEvent:
,这个实例方法是有数据源的,即一个遵循了Event
协议的对象.为了控制测试,只把对实例方法的测试作为变量,我们需要把数据源隔离起来,所以,我们fake了一个数据源,stub了它的协议方法,这样做到了真正的单元测试.
另外,一个测试主要用来说明sent_messages及reset_sent_messages 的用法.
源码:
#import <Cedar/Cedar.h>
#import "Sum.h"
using namespace Cedar::Matchers;
using namespace Cedar::Doubles;
SPEC_BEGIN(SumSpec)
describe(@"Sum", ^{
__block Sum<CedarDouble> *fakeSum;
__block int sum;
beforeEach(^{
fakeSum = nice_fake_for([Sum class]);
fakeSum stub_method("sumOfThreeNumbers:number2:number3:").and_return(10);
sum = [fakeSum sumOfThreeNumbers:1 number2:2 number3:3];
// 如果解开注释,调用会清零重置,所以下面的 "messages.count should equal(1);" 会报错 "Expected <0> to equal <1>"
// [fakeSum reset_sent_messages];
});
it(@"send messages", ^{
NSArray *messages = [fakeSum sent_messages];
messages.count should equal(1);
NSInvocation *firstInvocation = messages.firstObject;
firstInvocation.selector should equal(@selector(sumOfThreeNumbers:number2:number3:));
int firstParameter = 0;
int secondParameter = 0;
int thirdParameter = 0;
// 参数捕获
// Indices 0 and 1 indicate the hidden arguments self and _cmd, respectively; these values can be retrieved directly with the target and selector methods. Use indices 2 and greater for the arguments normally passed in a message.
[firstInvocation getArgument:&firstParameter atIndex:2];
firstParameter should equal(1);
[firstInvocation getArgument:&secondParameter atIndex:3];
secondParameter should equal(2);
[firstInvocation getArgument:&thirdParameter atIndex:4];
thirdParameter should equal(3);
});
});
SPEC_END