Hulk' Den

in-depth thinking and keep moving.

iOS Unit Tests 实现方案分析

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

S.O.L.I.D

理解单元测试,首先要理解单元(Unit):软件设计的最小单位,而在我们面向对象的世界里最小单位就是方法,所以好的单元测试一定很好的覆盖了方法。想覆盖就得有暴露,如果一个类包含了本应该设计成三个类的代码,首先它是不OOP的,其次它也是不可单元测试的(除非暴露私有方法)。所以好的代码结构和好的单元测试是相辅相成,互相依赖的。所以在文章的开始需要强调一下基础,面向对象的五大原则SOLID:

  • S - 单一职责原则,类的功能应该单一。
  • O - 开放封闭原则,对扩展开放,对修改封闭。
  • L - 里氏替换原则,子类可以完全代替父类在程序中运行。
  • I - 接口隔离原则,客户端不应去实现它们不需要的接口。
  • D - 依赖倒置原则,依赖于抽象而不是具体。

TDD or BDD ?

TDD

Test-driven development,测试驱动开发。 在我们设计一个类时,首先需要考虑这个类能提供什么功能以及如何提供,也就是这个类公开的方法。TDD的做法是先为这些公开的方法写测试代码,肯定一开始测试是通不过的,接下来开发人员的目的就是写代码实现类功能来让测试通过,测试通过了,这个类也就实现好了。

BDD

Behavior-driven development,行为驱动开发。 TDD的目的是测试每个公开方法的正确性,而BDD关注的是行为,BDD的测试过程是以陈述需求的方式进行的(Given..When..Then),所以更友好,也更接近实际需求。

举个栗子

在实现直播间内活动挂件按优先级显示的功能时,我们需要维护一个描述view具体信息的model的集合类来保存当前直播间所有view的情况以便调整,它需要满足以下四个功能:

//添加
-(void)addModel:(RHShowPriorityModel*)model;
//删除
-(void)removeModel:(RHShowPriorityModel*)model;
//通过view索引model
-(RHShowPriorityModel*)modelForView:(UIView*)view;
//通过position索引model
-(NSArray<RHShowPriorityModel*>*)modelsForPosition:(RHShowPriorityPosition)position;

以TDD的思想编写测试的话,就是聚焦到每一个公开的方法,设法进行全面的测试,我们使用官方的XCTest框架进行演练,代码如下:

- (void)setUp {
    [super setUp];
    
    self.modelA = [[RHShowPriorityModel alloc] init];
    self.viewA = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    _modelA.view = _viewA;
    _modelA.position = RHShowPriorityPositionLeftUp;
    
    self.modelB = [[RHShowPriorityModel alloc] init];
    self.viewB = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    _modelB.view = _viewB;
    _modelB.position = RHShowPriorityPositionLeftUp;
    
    self.collection = [[RHShowPriorityCollection alloc] initWithModels:[NSArray arrayWithObjects:_modelA, _modelB, nil]];
}

- (void)tearDown {
    [super tearDown];
}


-(void)testModelForView {
    XCTAssertEqual([_collection modelForView:_viewA], _modelA);
    XCTAssertEqual([_collection modelForView:_viewB], _modelB);
}

-(void)testModelsForPosition {
    NSArray *array = [_collection modelsForPosition:RHShowPriorityPositionLeftUp];
    XCTAssertTrue([array isKindOfClass:[NSArray class]]);
    XCTAssertEqual(array[0], _modelA);
    XCTAssertEqual(array[1], _modelB);
}

-(void)testAddAndRemoveModel {
    RHShowPriorityModel *addModel = [[RHShowPriorityModel alloc] init];
    UIView *addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 150, 150)];
    addModel.view = addView;
    XCTAssertNil([_collection modelForView:addView]);
    
    //test addModel
    [_collection addModel:addModel];
    XCTAssertEqual([_collection modelForView:addView], addModel);
    
    //test removeModel
    [_collection removeModel:addModel];
    XCTAssertNil([_collection modelForView:addView]);
}

以BDD的思想进行测试的话,应该聚焦到需求,可以试着以Given..When..Then的方式陈述一下我们对于这个集合类的需求,比如给你一个Coloection类,当它被初始化后,应该包含0个元素。我们使用比较热门的Kiwi框架GitHub - kiwi-bdd/Kiwi: Simple BDD for iOS 进行演练,代码如下:

describe(@"RHShowPriorityCollection", ^{
    context(@"当用初始数据modelA和modelB进行初始化后", ^{
        __block RHShowPriorityCollection *collection = nil;
        RHShowPriorityModel *modelA = [[RHShowPriorityModel alloc] init];
        UIView *viewA = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
        modelA.view = viewA;
        modelA.position = RHShowPriorityPositionLeftUp;
        
        RHShowPriorityModel *modelB = [[RHShowPriorityModel alloc] init];
        UIView *viewB = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        modelB.view = viewB;
        modelB.position = RHShowPriorityPositionLeftUp;
        
        beforeEach(^{
            collection = [[RHShowPriorityCollection alloc] initWithModels:[NSArray arrayWithObjects:modelA, modelB, nil]];
        });
        
        afterEach(^{
            collection = nil;
        });
        
        
        it(@"通过viewA可以得到modelA,通过viewB可以得到modelB。", ^{
            [[[collection modelForView:viewA] should] equal:modelA];
            [[[collection modelForView:viewB] should] equal:modelB];
        });
        
        it(@"通过一个位置可以得到一个数组,包含所有在这个位置的view的model。", ^{
            NSArray *array = [collection modelsForPosition:RHShowPriorityPositionLeftUp];
            [[array should] beKindOfClass:[NSArray class]];
            [[array[0] should] equal:modelA];
            [[array[1] should] equal:modelB];
        });
        
        it(@"可以成功添加一个model。", ^{
            RHShowPriorityModel *addModel = [[RHShowPriorityModel alloc] init];
            UIView *addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 150, 150)];
            addModel.view = addView;
            [[[collection modelForView:addView] should] beNil];
            [collection addModel:addModel];
            [[[collection modelForView:addView] should] equal:addModel];
        });
        
        it(@"也可以成功删除一个model。", ^{
            [[[collection modelForView:viewB] shouldNot] beNil];
            [collection removeModel:modelB];
            [[[collection modelForView:viewB] should] beNil];
        });
        
    });
});

惊不惊喜?反正在写的过程中我的内心是很舒服的,这段代码可以说是一目了然,清晰的陈述了我们对于这个类的要求,如果一位新同学要接手这块也是轻而易举。

最后回到我们的项目我认为:

对于像业务层的网络请求方法,只牵扯到基本的请求返回逻辑,测试也就只需要关注返回数据的正确与否,可以选择使用TDD的思想来做,也就是用官方的XCTest框架。

对于像上文中描述的场景,承载一个功能的类,包含比较丰富的逻辑在里面,使用BDD的思想更合适,写起来舒服,读起来清晰。

Stub、Mock

未完待续…

最近的文章

iOS UI Tests 实现方案分析

UI Tests是一个可以对UI进行测试的框架。为什么需要UI Tests呢?如果客户端分为基础层和业务层的话,业务层最终都是负责界面展示的,通常是MV(C/P/VM),(C/P/VM)中的逻辑并不适合用Unit Tests来覆盖。对于具体的界面操作流程,UI Tests是合适的选择。集成UI Tests下图为在Xcode中为项目添加UI Tests的步骤: 新建Target 新建Case ClassWriting UI TestsRecordingXcode为我们提供了把整套操作转化...…

iOS | Test继续阅读
更早的文章

当我谈旅行时,我谈些什么

随心在昆明的第二天,还有没去的但不想去的景点,心里总觉得憋得慌缺点啥,在云南大学周边转了一上午,吃过午饭后就彻底无聊烦躁到极点了,这才是旅途的第二天就这状态了,不合适啊。安静下来想了想,是因为昨天在滇池旁没玩够就去了民族村,本打算逛完民族村再去滇池旁诗意一会坐坐船看看西山风景,结果民族村太好玩导致逛完后体力都消耗殆尽了,加上阴天的昆明湿冷湿冷的,出了民族村我们就直接打道回府了,仿佛在心里种下了不痛快的种子,所以今天内心一直在那闹情绪,无聊烦躁没目的,像极了平常生活中那种麻木消极的状态。缕清...…

旅行跑步继续阅读