没有单元测试,何谈重构
2016-11-23 09:49
316 查看
最近科技公司流年不利,那边与整个硅谷唱反调的川普逆袭上台了,这边特斯拉被评为美国最不可靠汽车品牌,据报道是因为特斯拉为Model X增加了过于复杂的功能(高科技多也怪我咯),如前门采用电动开启方式,中排座椅实现了电动移动,所有这些功能整合在一个平台上,导致可靠性下滑。通俗解释下就是电动门有个小bug,电动座椅又有个小bug,一堆小bug最终导致的大bug,人命关天了,本篇就来谈谈软件开发中避免小bug的技术:单元测试。
本文将介绍以下内容:
iOS开发中添加单元测试的方法。
如何写单元测试用例及用例组。
介绍单元测试的一些基础概念。
本篇作为重构的例子(想了解重构是什么,另参见他们总在说重构,不过是重写 ),假设了一个视频网站的电影点播系统,每次点击播放就会收取费用,按电影种类不同,时段不同,则收费不同,最终计算出顾客的总消费,并计算积分。这个例子的类关系比较清晰易懂,用OC语言实现,iOS开发的童鞋看起来会比较亲切,心急的童鞋可以跳过源码部分,先看后面添加单元测试的部分准备测试工具,需要了解细节时再回头看源码。
系统包含一个电影类,顾客类,及点播类,类关系如下图所示:
电影类
点播类定义了点播行为,关心点播了什么电影,及点播的时段,这些都影响最终收取的费用。
这里选用的是XCTest,它是Xcode8中内置的测试框架,使用起来非常简单,分以下两种情况为项目添加测试:
1. 新建工程时添加单元测试:
新建时添加单元测试
2.为已有工程添加单元测试
Xcode8中添加的步骤与前几代有所不同:
添加Target
用关键词test快速找到Unit Testing bundle
添加好单元测试后的工程结构
添加第一个测试
第一个测试是很重要的,它决定了我们后面测试的思路和方向,这里以需要什么测什么为指导原则,从结果出发,所以先来看下基本的点播需求:
工作日点播一部普通影片,收费2元,积一分。
根据以上需求描述,我们在RefactorDemoTests.m添加测试方法:
按快捷键?U,运行测试,发现测试报错了:
第一次运行测试报错了
仔细检查发现,statment()的实现中,总价与单位没有空一格,斟酌后觉得还是空一格比较清晰,于是修改后,再次按快捷键?U运行测试,测试通过:
测试通过了
在单元测试中,绿色表示测试通过,红色表示测试失败,已经成为业界标准,XCTest遵循了这一规则。
测试用例组
通过第一个例子,我们知道了测试用例总是以test开头,作为约定俗成,凡是test开头的方法,都会被XCTest框架自动运行,下面我们添加对周末点播优惠的测试:
这时,我们已经有两个测试用例了,为了加快测试速度,打开Xcode左侧第5项的测试导航面板,可以单独指定一个用例运行,注意图中标记处的图标变化:
单独运行一个测试用例
如此,我们可以将statement需要考虑的返回情况都写成一个个都测试用例(这里就不一一列举了,童鞋们可以自行实现,有问题可以评论中提出,虽然我不一定会回答),可以确保报表算法满足全部需求。
单元测试和功能测试的差别
功能测试的目的是保证整个软件包能正常工作,它面向的对象是客户,保障软件功能符合客户的要求的质量,当然这类工作应该交由喜爱找bug的专业测试部门去处理,他们会用与开发截然不同的工具,并且不关心实现的细节(这就是你与测试人员老是话不投机的原因)。
而单元测试关注实现的细节,它的目标对象是一个类,一个方法,是我们开发人员用来验证代码是否有实现异常的工具,因此写单元测试时总是寻找那些可能未处理的边界。
测试循环
从上面的简单用例中,我们能明显看到以下通用步骤:
准备测试数据。
调用目标API
验证输出和行为
测试循环
小结
本文通过一个电影点播系统的例子,演示了以下内容:
iOS开发中添加单元测试框架XCTest。
用test方法组织单元测试用例及用例组,即可统一运行,也可单独运行。
介绍单元测试的一些基础概念,了解单元测试的目标,及测试循环。
这些是将来进一步的重构的基础和前提,限于篇幅,仿造对象等单元测试技术还未提及,欢迎关注溪石,且听下回分解。
本文将介绍以下内容:
iOS开发中添加单元测试的方法。
如何写单元测试用例及用例组。
介绍单元测试的一些基础概念。
本篇作为重构的例子(想了解重构是什么,另参见他们总在说重构,不过是重写 ),假设了一个视频网站的电影点播系统,每次点击播放就会收取费用,按电影种类不同,时段不同,则收费不同,最终计算出顾客的总消费,并计算积分。这个例子的类关系比较清晰易懂,用OC语言实现,iOS开发的童鞋看起来会比较亲切,心急的童鞋可以跳过源码部分,先看后面添加单元测试的部分准备测试工具,需要了解细节时再回头看源码。
系统包含一个电影类,顾客类,及点播类,类关系如下图所示:
电影类
// // Movie.h // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // typedef NS_ENUM(NSUInteger, MovieEnum) { MovieEnumChildrens = 2, MovieEnumRegular = 0, MovieEnumNewRelease = 1 }; @class Movie; @interface Movie : NSObject @property(nonatomic, copy) NSString *title; @property(nonatomic) int priceCode; - (id)initWithTitle:(NSString *)title priceCode:(int)priceCode; @end
// // Movie.m // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // #import "Movie.h" @implementation Movie - (id)initWithTitle:(NSString *)title priceCode:(int)priceCode { self = [super init]; if (self) { _title = title; _priceCode = priceCode; } return self; } @end点播类:
点播类定义了点播行为,关心点播了什么电影,及点播的时段,这些都影响最终收取的费用。
// // Demand.h // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // #import typedef NS_ENUM(NSUInteger, TimePeriodEnum) { TimePeriodEnumWorkDaytime = 1, TimePeriodEnumWorkNight = 2, TimePeriodEnumWeekend = 3 }; @class Movie; @interface Demand : NSObject @property(nonatomic) Movie *movie; @property(nonatomic, assign) int timePeriod; - (id)initWithMovie:(Movie *)movie timePeriod:(TimePeriodEnum)timePeriod; @end
// // Demand.m // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // #import "Demand.h" #import "Movie.h" @implementation Demand - (id)initWithMovie:(Movie *)movie timePeriod:(TimePeriodEnum)timePeriod { self = [super init]; if (self) { _movie = movie; _timePeriod = timePeriod; } return self; } @end顾客类
// // Customer.h // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // #import @class Demand; @interface Customer : NSObject - (id)initCustomerWithName:(NSString *)name; - (void)addDemand:(Demand *)demand; - (NSString *)statement; @end
// // Customer.m // RefactorDemo // // Created by xishi on 16/10/29. // Copyright ? 2016年 xs. All rights reserved. // #import "Customer.h" #import "Demand.h" #import "Movie.h" @interface Customer () { NSString *_name; NSMutableArray *_demands; } @end @implementation Customer - (id)initCustomerWithName:(NSString *)name { self = [super init]; if (self) { _name = name; } return self; } - (void)addDemand:(Demand *)demand { if (!_demands) { _demands = [[NSMutableArray alloc] init]; } [_demands addObject:demand]; } - (NSString *)statement { double totalAmount = 0; int frequentDemandPotnts = 0; NSMutableString *result = [NSMutableString stringWithFormat:@"%@的点播清单\\\\n", _name]; for (Demand *aDemand in _demands) { double thisAmount = 0; // 根据不同电影定价: switch (aDemand.movie.priceCode) { case MovieEnumRegular: thisAmount += 2; // 普通电影2元一次 break; case MovieEnumNewRelease: thisAmount += 3; // 新电影3元一次 break; case MovieEnumChildrens: thisAmount += 1.5; // 儿童电影1.5元一次 } // 根据不同时段定价: if (aDemand.timePeriod == TimePeriodEnumWorkDaytime) thisAmount *= 1.0; // 工作日全价 else if (aDemand.timePeriod == TimePeriodEnumWeekend) { thisAmount *= 0.5; // 周末半价 } else if (aDemand.timePeriod == TimePeriodEnumWorkNight){ thisAmount *= 1.5; // 下班1.5倍 } frequentDemandPotnts++; // 周末点播新片积分翻倍: if ((aDemand.movie.priceCode == MovieEnumNewRelease) && aDemand.timePeriod == TimePeriodEnumWeekend) { frequentDemandPotnts++; } [result appendFormat:@"\\\\t%@\\\\t%@ 元\\\\n", aDemand.movie.title, @(thisAmount)]; totalAmount += thisAmount; } [result appendFormat:@"费用总计 %@ 元\\\\n", @(totalAmount).stringValue]; [result appendFormat:@"获得积分 %@", @(frequentDemandPotnts).stringValue]; return result; } @end准备测试工具
这里选用的是XCTest,它是Xcode8中内置的测试框架,使用起来非常简单,分以下两种情况为项目添加测试:
1. 新建工程时添加单元测试:
新建时添加单元测试
2.为已有工程添加单元测试
Xcode8中添加的步骤与前几代有所不同:
添加Target
用关键词test快速找到Unit Testing bundle
添加好单元测试后的工程结构
添加第一个测试
第一个测试是很重要的,它决定了我们后面测试的思路和方向,这里以需要什么测什么为指导原则,从结果出发,所以先来看下基本的点播需求:
工作日点播一部普通影片,收费2元,积一分。
根据以上需求描述,我们在RefactorDemoTests.m添加测试方法:
- (void)testStatement_Regular { Movie *matrixMovie1 = [[Movie alloc] initWithTitle:@"黑客帝国1" priceCode:MovieEnumRegular]; Demand *aDemand1 = [[Demand alloc] initWithMovie:matrixMovie1 timePeriod:TimePeriodEnumWorkDaytime]; // 顾客租赁一部: Customer *aCustomer = [[Customer alloc] initCustomerWithName:@"溪石"]; [aCustomer addDemand:aDemand1]; XCTAssertTrue([@"溪石的点播清单\\\\n" @"\\\\t黑客帝国1\\\\t2 元\\\\n" @"费用总计 2 元\\\\n" @"获得积分 1" isEqualToString:[aCustomer statement]], @"测试点播一部普通电影"); }这个测试用例中,顾客“溪石”点播了一部老片《黑客帝国1》,由于是工作日,因此按原价收取,并积1分,详细细节看Cutomer类源码中的方法statement()。
按快捷键?U,运行测试,发现测试报错了:
第一次运行测试报错了
仔细检查发现,statment()的实现中,总价与单位没有空一格,斟酌后觉得还是空一格比较清晰,于是修改后,再次按快捷键?U运行测试,测试通过:
测试通过了
在单元测试中,绿色表示测试通过,红色表示测试失败,已经成为业界标准,XCTest遵循了这一规则。
测试用例组
通过第一个例子,我们知道了测试用例总是以test开头,作为约定俗成,凡是test开头的方法,都会被XCTest框架自动运行,下面我们添加对周末点播优惠的测试:
- (void)testStatement_Weekend { Movie *matrixMovie2 = [[Movie alloc] initWithTitle:@"黑客帝国2-重装上阵" priceCode:MovieEnumRegular]; Demand *aDemand2 = [[Demand alloc] initWithMovie:matrixMovie2 timePeriod:TimePeriodEnumWeekend]; Customer *aCustomer = [[Customer alloc] initCustomerWithName:@"溪石"]; [aCustomer addDemand:aDemand2]; XCTAssertTrue([@"溪石的点播清单\\\\n" @"\\\\t黑客帝国2-重装上阵\\\\t1 元\\\\n" @"费用总计 1 元\\\\n" @"获得积分 1" isEqualToString:[aCustomer statement]], @"测试点播一部普通电影,周末半价"); }这个测试用例除了电影名称不一样外,只是将点播时段由工作日改为了周末,以此判断计算规则是否正确。
这时,我们已经有两个测试用例了,为了加快测试速度,打开Xcode左侧第5项的测试导航面板,可以单独指定一个用例运行,注意图中标记处的图标变化:
单独运行一个测试用例
如此,我们可以将statement需要考虑的返回情况都写成一个个都测试用例(这里就不一一列举了,童鞋们可以自行实现,有问题可以评论中提出,虽然我不一定会回答),可以确保报表算法满足全部需求。
单元测试和功能测试的差别
功能测试的目的是保证整个软件包能正常工作,它面向的对象是客户,保障软件功能符合客户的要求的质量,当然这类工作应该交由喜爱找bug的专业测试部门去处理,他们会用与开发截然不同的工具,并且不关心实现的细节(这就是你与测试人员老是话不投机的原因)。
而单元测试关注实现的细节,它的目标对象是一个类,一个方法,是我们开发人员用来验证代码是否有实现异常的工具,因此写单元测试时总是寻找那些可能未处理的边界。
测试循环
从上面的简单用例中,我们能明显看到以下通用步骤:
准备测试数据。
调用目标API
验证输出和行为
测试循环
小结
本文通过一个电影点播系统的例子,演示了以下内容:
iOS开发中添加单元测试框架XCTest。
用test方法组织单元测试用例及用例组,即可统一运行,也可单独运行。
介绍单元测试的一些基础概念,了解单元测试的目标,及测试循环。
这些是将来进一步的重构的基础和前提,限于篇幅,仿造对象等单元测试技术还未提及,欢迎关注溪石,且听下回分解。
相关文章推荐
- 没有单元测试会很耽误事
- 为何没有单元测试就没有高质量代码?
- 如果创建工程的时候没有勾选Unit Test选项,如何创建单元测试 正确删除test target
- .NET重构—单元测试重构
- .NET项目开发—浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)
- .NET重构―单元测试重构
- 单元测试培训系列:(三)可测试性(Testability)与重构Refactoring
- .NET项目开发―浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)
- maven的单元测试中没有
- 没有单元测试,怎能写代码
- .NET重构—单元测试重构
- 《重构》 — Java示例:影片出租店程序(5、单元测试)
- .NET项目开发—浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)
- 《重构》 — Delphi示例:影片出租店程序(5、单元测试)
- 单元测试和设计模式在重构中的应用
- 【重构】DataTable"在位置0处没有任何行"
- .NET项目开发—浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)
- 遗留代码单元测试与重构的一点小体会
- .NET项目开发—浅谈面向接口编程、可测试性、单元测试、迭代重构(项目小结)