【CodeTest】Cedar介绍

学习资料

Cedar介绍

Cedar是OC开发中,BBD风格的一个主流单元测试框架,关于BBD的介绍可以参考这篇文章.

CocoaPods安装Cedar
target 'MyAppTests' do
  pod 'Cedar'
end  

可以利用Alcatraz安装Cedar测试文件模板.

语法简介

参见Writing specs

#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语法

参见Writing specs

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

下载源码

下载地址

推荐阅读更多精彩内容