SailorHub

iOS-进阶(12) : 单元测试-2

本文将介绍以下内容:

  1. iOS开发中添加单元测试的方法。
  2. 如何写单元测试用例及用例组。
  3. 介绍单元测试的一些基础概念。

本篇作为重构的例子,假设了一个视频网站的电影点播系统,每次点击播放就会收取费用,按电影种类不同,时段不同,则收费不同,最终计算出顾客的总消费,并计算积分。这个例子的类关系比较清晰易懂,用OC语言实现,iOS开发的童鞋看起来会比较亲切,心急的童鞋可以跳过源码部分,先看后面添加单元测试的部分,需要了解细节时再回头看源码。

系统包含一个电影类,顾客类,及点播类,类关系如下图所示:

电影类

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

#import “Movie.h”

@implementation Movie

– (id)initWithTitle:(NSString *)title

priceCode:(int)priceCode {

self = [super init];

if (self) {

_title = title;

_priceCode = priceCode;

}

return self;

}

@end

点播类:

点播类定义了点播行为,关心点播了什么电影,及点播的时段,这些都影响最终收取的费用。

port 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

#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

顾客类

#import @class Demand;

@interface Customer : NSObject

– (id)initCustomerWithName:(NSString *)name;

– (void)addDemand:(Demand *)demand;

– (NSString *)statement;

@end

#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. 介绍单元测试的一些基础概念,了解单元测试的目标,及测试循环。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注