(一) 初探 iOS 单元测试

何为单元测试

  单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。
  每个理想的测试案例独立于其它case,测试时需隔离模块。单元测试通常由软件开发人员编写,用于确保所写的代码匹配软件需求和遵循开发目标。它的实施方式可以是手动的,或是构建自动化的一部分。
  单元测试允许程序员在未来重构代码,且确保模块依然工作正确。这个过程是为所有方法编写单元测试,一旦变更导致错误发生,借助于单元测试可以快速定位并修复错误。
可读性强的单元测试可以使程序员方便地检查代码片断是否依然正常工作。良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。在连续的单元测试环境,通过其固有的持续维护工作,单元测试可以延续用于准确反映当任何变更发生时可执行程序和代码的表现。借助于上述开发实践和单元测试的覆盖,可以总是维持准确性。

单元测试的目的

1. 保证代码的质量

  代码可以通过编译器检查语法的正确性,却不能保证代码逻辑是正确的,尤其包含了许多单元分支的情况下,单元测试可以保证代码的行为和结果与我们的预期和需求一致。在测试某段代码的行为是否和你的期望一致时,你需要确认,在任何情况下,这段代码是否都和你的期望一致,譬如参数可能为空,可能的异步操作等。

2. 保证代码的可维护性

  保证原有单元测试正确的情况下,无论如何修改单元内部代码,测试的结果应该是正确的,且修改后不会影响到其他的模块。

3. 保证代码的可扩展性

  为了保证可行的可持续的单元测试,程序单元应该是低耦合的,否则,单元测试将难以进行。

单元测试的本质

1. 是一种验证行为

  单元测试在开发前期检验了代码逻辑的正确性,开发后期,无论是修改代码内部抑或重构,测试的结果为这一切提供了可量化的保障。

2. 是一种设计行为

  为了可进行单元测试,尤其是先写单元测试(TDD),我们将从调用者思考,从接口上思考,我们必须把程序单元设计成接口功能划分清晰的,易于测试的,且与外部模块耦合性尽可能小。

3. 是一种快速回归的方式

  在原代码基础上开发及修改功能时,单元测试是一种快捷,可靠的回归。

4. 是程序优良的文档

  从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

-----------------------------------------

*两种测试思想

  测试驱动开发(Test-driven development,TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。

  行为驱动开发(Behavior-driven development,BDD)是一种敏捷软件开发的技术,BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了 测试驱动开发方法(TDD)。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

在iOS单元测试框架中,kiwi是BDD的代表。

-----------------------------------------

初探 iOS 单元测试

XCTest

  Xcode集成了对单元测试的支持,XCode4.x集成的是OCUnit,到了XCode5.x时代就升级为了XCTest,XCode7.x时代XCtest还可以进行UI测试。下面我们简单介绍下XCTest的使用。

  在xcode新建项目中,默认会建一个单元测试的target,并建立一个继承于XCTestCase的测试用例类
XCTest-Target
  若项目中没有,可以在 File->New->Target->ios-test->iOS Unit Testing Bundle 新建一个测试target。
Target-New

  本例实现了一个个税计算方法,在测试用例中测试输入后输出是否符合结果。

ASUnitTestFirstDemoTests.m
#import <XCTest/XCTest.h>
#import "ASRevenueBL.h"

@interface ASUnitTestFirstDemoTests : XCTestCase

@property (nonatomic, strong) ASRevenueBL *revenueBL;

@end

@implementation ASUnitTestFirstDemoTests

- (void)setUp {
    [super setUp];
    self.revenueBL = [[ASRevenueBL alloc] init];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    self.revenueBL = nil;

    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}

- (void)testLevel1 {      // 异步测试
  double revenue = 5000;
  double tax = [self.revenueBL calculate:revenue];
  XCTAssertEqual(tax, 45.0, @"用例1测试失败");
  XCTAssertTrue(tax == 45.0);
}

- (void)testLevel2 {
  XCTestExpectation *exp = [self expectationWithDescription:@"超时"];
  NSOperationQueue *queue = [[NSOperationQueue alloc]init];
  [queue addOperationWithBlock:^{
    double revenue = 1500;
    double tax = [self.revenueBL calculate:revenue];
    sleep(1);
    XCTAssertEqual(tax, 45.0, @"用例2测试失败");
    [exp fulfill];  // exp结束
  }];
  
  [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
    if (error) {
      NSLog(@"Timeout Error: %@", error);
    }
  }];
}
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
       for (int a = 0; a<10000; a+=a) {
            NSLog(@"%zd", a);
      }
    }];
}

@end

ASRevenueBL.m
#import "ASRevenueBL.h"

#define baseNum 3500.0

@implementation ASRevenueBL

- (double)calculate:(double)revenue {
  double tax = 0.0;
  double dbTaxRevenue = revenue - baseNum;
  if (dbTaxRevenue <= 1500) {
    tax = dbTaxRevenue * 0.03;
  } else if (dbTaxRevenue > 1500 && dbTaxRevenue <= 4500) {
    tax = dbTaxRevenue * 0.1 - 105;
  } else if (dbTaxRevenue > 4500 && dbTaxRevenue <= 9000) {
    tax = dbTaxRevenue * 0.2 - 555;
  } else if (dbTaxRevenue > 9000 && dbTaxRevenue <= 35000) {
    tax = dbTaxRevenue * 0.25 - 1005;
  } else if (dbTaxRevenue > 35000 && dbTaxRevenue <= 55000) {
    tax = dbTaxRevenue * 0.3 - 2755;
  } else if (dbTaxRevenue > 55000 && dbTaxRevenue <= 80000) {
    tax = dbTaxRevenue * 0.35 - 5505;
  } else if (dbTaxRevenue > 80000) {
    tax = dbTaxRevenue * 0.45 - 13505;
  }
  return tax;
}

@end
XCTest常用方法介绍:
 - (void)setUp; // 测试开始前调用,可以初始化一些对象和变量
 - (void)tearDown; // 测试结束后调用
 - (void)test##Name; // 含有test前缀无参数无返回的方法都为一个测试方法
 - (void)measureBlock:((void (^)(void)))block;  // 测量执行时间
 - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler; // 多少秒exception不fullfill就报错
 - (XCTestExpectation *)expectationForNotification:(NSNotificationName)notificationName object:(nullable id)objectToObserve handler:(nullable XCNotificationExpectationHandler)handler;  // 匹配到通知fullfill
 - (XCTestExpectation *)expectationForPredicate:(NSPredicate *)predicate evaluatedWithObject:(id)object handler:(nullable XCPredicateExpectationHandler)handler;  // predicate 返回true测试fullfill
...
测试结果

product-test 或 command + u即启动test
测试结果1
测试结果2
 ** 常用断言 **
XCTAssertNil(a1, ...)为空判断,expression为空时通过
XCTAssert(expression, ...)当expression值为TRUE时通过;
XCTAssertTrue(expression, format...)当expression值为TRUE时通过;
XCTAssertEqual(e1, e2, ...) e1 == e2通过;
XCTAssertThrows(expression, format...)当expression抛出异常时通过;
XCTAssertThrowsSpecific(expression, specificException, format...) 当expression抛出specificException异常时通过;

testLevel1通过revenueBL计算出来的tax与预期相同,测试通过;testLevel2通过revenueBL计算出来的tax与预期不同,测试不通过,反映出了程序一些逻辑漏洞;testPerformanceExample中的平均执行时间比基准值低,测试通过。

命令行

在命令行中也可以启动测试,便于持续集成。

Assuner$ cd Desktop/
Desktop Assuner$ cd ASUnitTestFirstDemo/
ASUnitTestFirstDemo Assuner$ xcodebuild test -project ASUnitTestFirstDemo.xcodeproj -scheme ASUnitTestFirstDemo -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' // 可以有多个destination

结果

Test Suite 'All tests' started at 2017-09-11 11:12:16.348
Test Suite 'ASUnitTestFirstDemoTests.xctest' started at 2017-09-11 11:12:16.349
Test Suite 'ASUnitTestFirstDemoTests' started at 2017-09-11 11:12:16.349
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' started.
Test Case '-[ASUnitTestFirstDemoTests testLevel1]' passed (0.001 seconds).
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' started.
/Users/liyongguang-eleme-iOS-Development/Desktop/ASUnitTestFirstDemo/ASUnitTestFirstDemoTests/ASUnitTestFirstDemoTests.m:46: error: -[ASUnitTestFirstDemoTests testLevel2] : ((tax) equal to (45.0)) failed: ("-60") is not equal to ("45") - 用例2测试失败
Test Case '-[ASUnitTestFirstDemoTests testLevel2]' failed (1.007 seconds).
Test Suite 'ASUnitTestFirstDemoTests' failed at 2017-09-11 11:12:17.358.
     Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.009) seconds
Test Suite 'ASUnitTestFirstDemoTests.xctest' failed at 2017-09-11 11:12:17.359.
     Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.010) seconds
Test Suite 'All tests' failed at 2017-09-11 11:12:17.360.
     Executed 2 tests, with 1 failure (0 unexpected) in 1.008 (1.012) seconds
Failing tests:
    -[ASUnitTestFirstDemoTests testLevel2]
** TEST FAILED **

如果是workspace

xcodebuild -workspace ASKiwiTest.xcworkspace -scheme ASKiwiTest-Example -destination 'platform=iOS Simulator,OS=11.0,name=iPhone 7' test

每个test方法都会跑一遍,并给出结果描述。

谢谢观看!如有错误请多指正

参考阅读

维基百科
man xcodebuild
XCTestCase
cocoaChina测试专题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容