最近科技公司流年不利婴削,那邊與整個(gè)硅谷唱反調(diào)的川普逆襲上臺了,這邊特斯拉被評為美國最不可靠汽車品牌牙肝,據(jù)報(bào)道是因?yàn)樘厮估瓰镸odel X增加了過于復(fù)雜的功能(高科技多也怪我咯)唉俗,如前門采用電動(dòng)開啟方式,中排座椅實(shí)現(xiàn)了電動(dòng)移動(dòng)配椭,所有這些功能整合在一個(gè)平臺上虫溜,導(dǎo)致可靠性下滑。通俗解釋下就是電動(dòng)門有個(gè)小bug股缸,電動(dòng)座椅又有個(gè)小bug衡楞,一堆小bug最終導(dǎo)致的大bug,人命關(guān)天了敦姻,本篇就來談?wù)勡浖_發(fā)中避免小bug的技術(shù):單元測試瘾境。
本文將介紹以下內(nèi)容:
- iOS開發(fā)中添加單元測試的方法。
- 如何寫單元測試用例及用例組镰惦。
- 介紹單元測試的一些基礎(chǔ)概念迷守。
本篇作為重構(gòu)的例子(想了解重構(gòu)是什么,另參見他們總在說重構(gòu)旺入,不過是重寫 )兑凿,假設(shè)了一個(gè)視頻網(wǎng)站的電影點(diǎn)播系統(tǒng),每次點(diǎn)擊播放就會收取費(fèi)用茵瘾,按電影種類不同礼华,時(shí)段不同,則收費(fèi)不同拗秘,最終計(jì)算出顧客的總消費(fèi)卓嫂,并計(jì)算積分。這個(gè)例子的類關(guān)系比較清晰易懂聘殖,用OC語言實(shí)現(xiàn),iOS開發(fā)的童鞋看起來會比較親切行瑞,心急的童鞋可以跳過源碼部分奸腺,先看后面添加單元測試的部分準(zhǔn)備測試工具,需要了解細(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)備測試工具
這里選用的是XCTest陨舱,它是Xcode8中內(nèi)置的測試框架,使用起來非常簡單版仔,分以下兩種情況為項(xiàng)目添加測試:
1. 新建工程時(shí)添加單元測試:
2.為已有工程添加單元測試
Xcode8中添加的步驟與前幾代有所不同:
添加第一個(gè)測試
第一個(gè)測試是很重要的游盲,它決定了我們后面測試的思路和方向,這里以需要什么測什么為指導(dǎo)原則蛮粮,從結(jié)果出發(fā)益缎,所以先來看下基本的點(diǎn)播需求:
工作日點(diǎn)播一部普通影片,收費(fèi)2元然想,積一分莺奔。
根據(jù)以上需求描述,我們在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([@"溪石的點(diǎn)播清單\\\\n"
@"\\\\t黑客帝國1\\\\t2 元\\\\n"
@"費(fèi)用總計(jì) 2 元\\\\n"
@"獲得積分 1"
isEqualToString:[aCustomer statement]],
@"測試點(diǎn)播一部普通電影");
}
這個(gè)測試用例中变泄,顧客“溪石”點(diǎn)播了一部老片《黑客帝國1》令哟,由于是工作日,因此按原價(jià)收取杖刷,并積1分励饵,詳細(xì)細(xì)節(jié)看Cutomer類源碼中的方法statement()。
按快捷鍵?U
滑燃,運(yùn)行測試役听,發(fā)現(xiàn)測試報(bào)錯(cuò)了:
仔細(xì)檢查發(fā)現(xiàn),statment()的實(shí)現(xiàn)中表窘,總價(jià)與單位沒有空一格典予,斟酌后覺得還是空一格比較清晰,于是修改后乐严,再次按快捷鍵?U
運(yùn)行測試瘤袖,測試通過:
在單元測試中,綠色表示測試通過昂验,紅色表示測試失敗捂敌,已經(jīng)成為業(yè)界標(biāo)準(zhǔn),XCTest遵循了這一規(guī)則既琴。
測試用例組
通過第一個(gè)例子占婉,我們知道了測試用例總是以test
開頭,作為約定俗成甫恩,凡是test開頭的方法逆济,都會被XCTest框架自動(dòng)運(yùn)行,下面我們添加對周末點(diǎn)播優(yōu)惠的測試:
- (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]],
@"測試點(diǎn)播一部普通電影,周末半價(jià)");
}
這個(gè)測試用例除了電影名稱不一樣外奖慌,只是將點(diǎn)播時(shí)段由工作日改為了周末抛虫,以此判斷計(jì)算規(guī)則是否正確。
這時(shí)简僧,我們已經(jīng)有兩個(gè)測試用例了建椰,為了加快測試速度,打開Xcode左側(cè)第5項(xiàng)的測試導(dǎo)航面板涎劈,可以單獨(dú)指定一個(gè)用例運(yùn)行广凸,注意圖中標(biāo)記處的圖標(biāo)變化:
如此,我們可以將statement需要考慮的返回情況都寫成一個(gè)個(gè)都測試用例(這里就不一一列舉了蛛枚,童鞋們可以自行實(shí)現(xiàn)谅海,有問題可以評論中提出,雖然我不一定會回答)蹦浦,可以確保報(bào)表算法滿足全部需求扭吁。
單元測試和功能測試的差別
功能測試的目的是保證整個(gè)軟件包能正常工作,它面向的對象是客戶盲镶,保障軟件功能符合客戶的要求的質(zhì)量侥袜,當(dāng)然這類工作應(yīng)該交由喜愛找bug的專業(yè)測試部門去處理,他們會用與開發(fā)截然不同的工具溉贿,并且不關(guān)心實(shí)現(xiàn)的細(xì)節(jié)(這就是你與測試人員老是話不投機(jī)的原因)枫吧。
而單元測試關(guān)注實(shí)現(xiàn)的細(xì)節(jié),它的目標(biāo)對象是一個(gè)類宇色,一個(gè)方法九杂,是我們開發(fā)人員用來驗(yàn)證代碼是否有實(shí)現(xiàn)異常的工具,因此寫單元測試時(shí)總是尋找那些可能未處理的邊界宣蠕。
測試循環(huán)
從上面的簡單用例中例隆,我們能明顯看到以下通用步驟:
- 準(zhǔn)備測試數(shù)據(jù)。
- 調(diào)用目標(biāo)API
- 驗(yàn)證輸出和行為
小結(jié)
本文通過一個(gè)電影點(diǎn)播系統(tǒng)的例子抢蚀,演示了以下內(nèi)容:
- iOS開發(fā)中添加單元測試框架XCTest镀层。
- 用test方法組織單元測試用例及用例組,即可統(tǒng)一運(yùn)行皿曲,也可單獨(dú)運(yùn)行唱逢。
- 介紹單元測試的一些基礎(chǔ)概念,了解單元測試的目標(biāo)屋休,及測試循環(huán)坞古。
這些是將來進(jìn)一步的重構(gòu)的基礎(chǔ)和前提,限于篇幅博投,仿造對象等單元測試技術(shù)還未提及,歡迎關(guān)注溪石盯蝴,且聽下回分解毅哗。