最近科技公司流年不利,那邊與整個(gè)硅谷唱反調(diào)的川普逆襲上臺(tái)了,這邊特斯拉被評(píng)為美國最不可靠汽車品牌,據(jù)報(bào)道是因?yàn)樘厮估瓰镸odel X增加了過于復(fù)雜的功能(高科技多也怪我咯),如前門采用電動(dòng)開啟方式,中排座椅實(shí)現(xiàn)了電動(dòng)移動(dòng),所有這些功能整合在一個(gè)平臺(tái)上,導(dǎo)致可靠性下滑。通俗解釋下就是電動(dòng)門有個(gè)小bug,電動(dòng)座椅又有個(gè)小bug,一堆小bug最終導(dǎo)致的大bug,人命關(guān)天了,本篇就來談?wù)勡浖_發(fā)中避免小bug的技術(shù):單元測(cè)試。
本文將介紹以下內(nèi)容:
- iOS開發(fā)中添加單元測(cè)試的方法。
- 如何寫單元測(cè)試用例及用例組。
- 介紹單元測(cè)試的一些基礎(chǔ)概念。
本篇作為重構(gòu)的例子(想了解重構(gòu)是什么,另參見他們總在說重構(gòu),不過是重寫 ),假設(shè)了一個(gè)視頻網(wǎng)站的電影點(diǎn)播系統(tǒng),每次點(diǎn)擊播放就會(huì)收取費(fèi)用,按電影種類不同,時(shí)段不同,則收費(fèi)不同,最終計(jì)算出顧客的總消費(fèi),并計(jì)算積分。這個(gè)例子的類關(guān)系比較清晰易懂,用OC語言實(shí)現(xiàn),iOS開發(fā)的童鞋看起來會(huì)比較親切,心急的童鞋可以跳過源碼部分,先看后面添加單元測(cè)試的部分準(zhǔn)備測(cè)試工具,需要了解細(xì)節(jié)時(shí)再回頭看源碼。
系統(tǒng)包含一個(gè)<u>電影類</u>,<u>顧客類</u>,及<u>點(diǎn)播類</u>,類關(guān)系如下圖所示:
<u>電影類</u>
//
// 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
<u>點(diǎn)播類</u>:
點(diǎn)播類定義了點(diǎn)播行為,關(guān)心點(diǎn)播了什么電影,及點(diǎn)播的時(shí)段,這些都影響最終收取的費(fèi)用。
//
// 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
<u>顧客類</u>
//
// 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:@"%@的點(diǎn)播清單\\\\n", _name];
for (Demand *aDemand in _demands) {
double thisAmount = 0;
// 根據(jù)不同電影定價(jià):
switch (aDemand.movie.priceCode) {
case MovieEnumRegular:
thisAmount += 2; // 普通電影2元一次
break;
case MovieEnumNewRelease:
thisAmount += 3; // 新電影3元一次
break;
case MovieEnumChildrens:
thisAmount += 1.5; // 兒童電影1.5元一次
}
// 根據(jù)不同時(shí)段定價(jià):
if (aDemand.timePeriod == TimePeriodEnumWorkDaytime)
thisAmount *= 1.0; // 工作日全價(jià)
else
if (aDemand.timePeriod == TimePeriodEnumWeekend) {
thisAmount *= 0.5; // 周末半價(jià)
}
else
if (aDemand.timePeriod == TimePeriodEnumWorkNight){
thisAmount *= 1.5; // 下班1.5倍
}
frequentDemandPotnts++;
// 周末點(diǎn)播新片積分翻倍:
if ((aDemand.movie.priceCode == MovieEnumNewRelease) &&
aDemand.timePeriod == TimePeriodEnumWeekend) {
frequentDemandPotnts++;
}
[result appendFormat:@"\\\\t%@\\\\t%@ 元\\\\n", aDemand.movie.title, @(thisAmount)];
totalAmount += thisAmount;
}
[result appendFormat:@"費(fèi)用總計(jì) %@ 元\\\\n", @(totalAmount).stringValue];
[result appendFormat:@"獲得積分 %@", @(frequentDemandPotnts).stringValue];
return result;
}
@end
<p id="jump"></p>
準(zhǔn)備測(cè)試工具
這里選用的是XCTest,它是Xcode8中內(nèi)置的測(cè)試框架,使用起來非常簡單,分以下兩種情況為項(xiàng)目添加測(cè)試:
1. 新建工程時(shí)添加單元測(cè)試:
2.為已有工程添加單元測(cè)試
Xcode8中添加的步驟與前幾代有所不同:
添加第一個(gè)測(cè)試
第一個(gè)測(cè)試是很重要的,它決定了我們后面測(cè)試的思路和方向,這里以需要什么測(cè)什么為指導(dǎo)原則,從結(jié)果出發(fā),所以先來看下基本的點(diǎn)播需求:
工作日點(diǎn)播一部普通影片,收費(fèi)2元,積一分。
根據(jù)以上需求描述,我們?cè)?code>RefactorDemoTests.m添加測(cè)試方法:
- (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([@"溪石的點(diǎn)播清單\\\\n"
@"\\\\t黑客帝國1\\\\t2 元\\\\n"
@"費(fèi)用總計(jì) 2 元\\\\n"
@"獲得積分 1"
isEqualToString:[aCustomer statement]],
@"測(cè)試點(diǎn)播一部普通電影");
}
這個(gè)測(cè)試用例中,顧客“溪石”點(diǎn)播了一部老片《黑客帝國1》,由于是工作日,因此按原價(jià)收取,并積1分,詳細(xì)細(xì)節(jié)看Cutomer類源碼中的方法statement()。
按快捷鍵?U
,運(yùn)行測(cè)試,發(fā)現(xiàn)測(cè)試報(bào)錯(cuò)了:
仔細(xì)檢查發(fā)現(xiàn),statment()的實(shí)現(xiàn)中,總價(jià)與單位沒有空一格,斟酌后覺得還是空一格比較清晰,于是修改后,再次按快捷鍵?U
運(yùn)行測(cè)試,測(cè)試通過:
在單元測(cè)試中,綠色表示測(cè)試通過,紅色表示測(cè)試失敗,已經(jīng)成為業(yè)界標(biāo)準(zhǔn),XCTest遵循了這一規(guī)則。
測(cè)試用例組
通過第一個(gè)例子,我們知道了測(cè)試用例總是以test
開頭,作為約定俗成,凡是test開頭的方法,都會(huì)被XCTest框架自動(dòng)運(yùn)行,下面我們添加對(duì)周末點(diǎn)播優(yōu)惠的測(cè)試:
- (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([@"溪石的點(diǎn)播清單\\\\n"
@"\\\\t黑客帝國2-重裝上陣\\\\t1 元\\\\n"
@"費(fèi)用總計(jì) 1 元\\\\n"
@"獲得積分 1"
isEqualToString:[aCustomer statement]],
@"測(cè)試點(diǎn)播一部普通電影,周末半價(jià)");
}
這個(gè)測(cè)試用例除了電影名稱不一樣外,只是將點(diǎn)播時(shí)段由工作日改為了周末,以此判斷計(jì)算規(guī)則是否正確。
這時(shí),我們已經(jīng)有兩個(gè)測(cè)試用例了,為了加快測(cè)試速度,打開Xcode左側(cè)第5項(xiàng)的測(cè)試導(dǎo)航面板,可以單獨(dú)指定一個(gè)用例運(yùn)行,注意圖中標(biāo)記處的圖標(biāo)變化:
如此,我們可以將statement需要考慮的返回情況都寫成一個(gè)個(gè)都測(cè)試用例(這里就不一一列舉了,童鞋們可以自行實(shí)現(xiàn),有問題可以評(píng)論中提出,雖然我不一定會(huì)回答),可以確保報(bào)表算法滿足全部需求。
單元測(cè)試和功能測(cè)試的差別
功能測(cè)試的目的是保證整個(gè)軟件包能正常工作,它面向的對(duì)象是客戶,保障軟件功能符合客戶的要求的質(zhì)量,當(dāng)然這類工作應(yīng)該交由喜愛找bug的專業(yè)測(cè)試部門去處理,他們會(huì)用與開發(fā)截然不同的工具,并且不關(guān)心實(shí)現(xiàn)的細(xì)節(jié)(這就是你與測(cè)試人員老是話不投機(jī)的原因)。
而單元測(cè)試關(guān)注實(shí)現(xiàn)的細(xì)節(jié),它的目標(biāo)對(duì)象是一個(gè)類,一個(gè)方法,是我們開發(fā)人員用來驗(yàn)證代碼是否有實(shí)現(xiàn)異常的工具,因此寫單元測(cè)試時(shí)總是尋找那些可能未處理的邊界。
測(cè)試循環(huán)
從上面的簡單用例中,我們能明顯看到以下通用步驟:
- 準(zhǔn)備測(cè)試數(shù)據(jù)。
- 調(diào)用目標(biāo)API
- 驗(yàn)證輸出和行為
小結(jié)
本文通過一個(gè)電影點(diǎn)播系統(tǒng)的例子,演示了以下內(nèi)容:
- iOS開發(fā)中添加單元測(cè)試框架XCTest。
- 用test方法組織單元測(cè)試用例及用例組,即可統(tǒng)一運(yùn)行,也可單獨(dú)運(yùn)行。
- 介紹單元測(cè)試的一些基礎(chǔ)概念,了解單元測(cè)試的目標(biāo),及測(cè)試循環(huán)。
這些是將來進(jìn)一步的重構(gòu)的基礎(chǔ)和前提,限于篇幅,仿造對(duì)象等單元測(cè)試技術(shù)還未提及,歡迎關(guān)注溪石,且聽下回分解。