美文网首页
【CodeTest】Cedar介绍

【CodeTest】Cedar介绍

作者: 刘大帅 | 来源:发表于2015-11-27 16:58 被阅读148次

    学习资料

    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
    

    下载源码

    下载地址

    相关文章

      网友评论

          本文标题:【CodeTest】Cedar介绍

          本文链接:https://www.haomeiwen.com/subject/zrvdhttx.html