您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  要资料 文章 文库 Lib 视频 Code iProcess 课程 认证 咨询 工具 火云堂 讲座吧   成长之路  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
     
   
 订阅
  捐助
没有单元测试,何谈重构
 
作者:溪石iOS 来源:简书 发布于: 2017-3-30
391 次浏览     评价:      
 

最近科技公司流年不利,那边与整个硅谷唱反调的川普逆袭上台了,这边特斯拉被评为美国最不可靠汽车品牌,据报道是因为特斯拉为Model X增加了过于复杂的功能(高科技多也怪我咯),如前门采用电动开启方式,中排座椅实现了电动移动,所有这些功能整合在一个平台上,导致可靠性下滑。通俗解释下就是电动门有个小bug,电动座椅又有个小bug,一堆小bug最终导致的大bug,人命关天了,本篇就来谈谈软件开发中避免小bug的技术:单元测试。

本文将介绍以下内容:

1.iOS开发中添加单元测试的方法。

2.如何写单元测试用例及用例组。

3.介绍单元测试的一些基础概念。

本篇作为重构的例子(想了解重构是什么,另参见他们总在说重构,不过是重写 ),假设了一个视频网站的电影点播系统,每次点击播放就会收取费用,按电影种类不同,时段不同,则收费不同,最终计算出顾客的总消费,并计算积分。这个例子的类关系比较清晰易懂,用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 <Foundation/Foundation.h>
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 <Foundation/Foundation.h>

@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的专业测试部门去处理,他们会用与开发截然不同的工具,并且不关心实现的细节(这就是你与测试人员老是话不投机的原因)。

而单元测试关注实现的细节,它的目标对象是一个类,一个方法,是我们开发人员用来验证代码是否有实现异常的工具,因此写单元测试时总是寻找那些可能未处理的边界。

测试循环

从上面的简单用例中,我们能明显看到以下通用步骤:

1.准备测试数据。

2.调用目标API

3.验证输出和行为

测试循环

小结

本文通过一个电影点播系统的例子,演示了以下内容:

1.iOS开发中添加单元测试框架XCTest。

2. 用test方法组织单元测试用例及用例组,即可统一运行,也可单独运行。

3. 介绍单元测试的一些基础概念,了解单元测试的目标,及测试循环。

这些是将来进一步的重构的基础和前提,限于篇幅,仿造对象等单元测试技术还未提及,欢迎关注溪石,且听下回分解。

   
 订阅
  捐助
相关文章

性能测试十问:测试经理篇
web前端性能优化进阶路
性能测试综述
VS2010中自动化测试—Web性能测试
相关文档

性能测试
性能测试执行之测试脚本录制
性能测试进阶1-流程篇
性能测试进阶2-方案篇
相关课程

性能测试方法与技术
使用LoadRunner进行性能测试
Android应用的性能测试
基于Android的单元、性能测试
 

LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术

相关咨询服务
建立软件测试规范
性能评测与调优


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...   
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号