没有单元测试,何谈重构

没有单元测试,何谈重构

 

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

本文将介绍以下内容:

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

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

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

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

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

2025746-974734521772d289.png

电影类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
//  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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
//  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

点播类:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//
//  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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
//  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

顾客类

1
2
3
4
5
6
7
8
9
10
11
12
13
//
//  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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//
//  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. 新建工程时添加单元测试:

2025746-f7ff1ad749130425.png

新建时添加单元测试

2.为已有工程添加单元测试

Xcode8中添加的步骤与前几代有所不同:

2025746-62f0ad0c01476f42.png

添加Target

2025746-ed065ed913109ceb.png

用关键词test快速找到Unit Testing bundle

2025746-7f1b95bda9c2c378.png

添加好单元测试后的工程结构

 

添加第一个测试

第一个测试是很重要的,它决定了我们后面测试的思路和方向,这里以需要什么测什么为指导原则,从结果出发,所以先来看下基本的点播需求:

工作日点播一部普通影片,收费2元,积一分。

根据以上需求描述,我们在RefactorDemoTests.m添加测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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,运行测试,发现测试报错了:

1.png

第一次运行测试报错了

仔细检查发现,statment()的实现中,总价与单位没有空一格,斟酌后觉得还是空一格比较清晰,于是修改后,再次按快捷键?U运行测试,测试通过:

2.png

测试通过了

在单元测试中,绿色表示测试通过,红色表示测试失败,已经成为业界标准,XCTest遵循了这一规则。

测试用例组

通过第一个例子,我们知道了测试用例总是以test开头,作为约定俗成,凡是test开头的方法,都会被XCTest框架自动运行,下面我们添加对周末点播优惠的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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项的测试导航面板,可以单独指定一个用例运行,注意图中标记处的图标变化:

3.png

单独运行一个测试用例

如此,我们可以将statement需要考虑的返回情况都写成一个个都测试用例(这里就不一一列举了,童鞋们可以自行实现,有问题可以评论中提出,虽然我不一定会回答),可以确保报表算法满足全部需求。

单元测试和功能测试的差别

功能测试的目的是保证整个软件包能正常工作,它面向的对象是客户,保障软件功能符合客户的要求的质量,当然这类工作应该交由喜爱找bug的专业测试部门去处理,他们会用与开发截然不同的工具,并且不关心实现的细节(这就是你与测试人员老是话不投机的原因)。

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

测试循环

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

  1. 准备测试数据。

  2. 调用目标API

  3. 验证输出和行为

4.png

测试循环

小结

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

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

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

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

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

转载于:https://www.cnblogs.com/ruixin-jia/p/6108161.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值