iOS - 单元测试

  • 简单使用单元测试
  • 对方法引用AFN框架的单元测试
    不写单元测试的程序员是不合格的,为了让自己成为一名合格的程序员,学习如何写单元测试是很有必要的,这里以Xcode集成的测试框架XCTest为例。

XCTest基础用法
默认的测试类继承自XCTestCase,当然也可以自定义测试类,添加一些公共的辅助方法,所有的测试方法都必须以test开头,且不能有参数,不然不会识别为测试方法。

@interface __Tests : XCTestCase

@end

@implementation __Tests

// 在每一个测试用例开始前调用,用来初始化相关数据
- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

// 在测试用例完成后调用,可以用来释放变量等结尾操作
- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}
// 测试方法
- (void)testExample {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct results.
}

// 性能测试方法,通过测试block中方法执行的时间,比对设定的标准值和偏差觉得是否可以通过测试
- (void)testPerformanceExample {
    // This is an example of a performance test case.
    [self measureBlock:^{
        // Put the code you want to measure the time of here.
    }];
}

/// 我自定义的 针对 Person 类的测试方法
- (void)testPerson{

}
@end

断言
XCTest的断言具体可查阅XCTestAssertions.h文件,这里还是做个简单的总结

//通用断言
XCTAssert(expression, format...)
//常用断言:
XCTAssertTrue(expression, format...)
XCTAssertFalse(expression, format...)
XCTAssertEqual(expression1, expression2, format...)
XCTAssertNotEqual(expression1, expression2, format...)
XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)
XCTAssertNil(expression, format...)
XCTAssertNotNil(expression, format...)

举个例子

- (void)testExample {
    //设置变量和设置预期值
    NSUInteger a = 10;NSUInteger b = 15;
    NSUInteger expected = 24;
    //执行方法得到实际值
    NSUInteger actual = [self add:a b:b];
    //断言判定实际值和预期是否符合
    XCTAssertEqual(expected, actual,@"add方法错误!");
}
-(NSUInteger)add:(NSUInteger)a b:(NSUInteger)b{
    return a+b;
}

从这也能看出一个测试用例比较规范的写法,1:定义变量和预期,2:执行方法得到实际值,3:断言

当然在有些特殊情况下直接使用这些断言,会让代码看起来很臃肿,使用时可以对其进行一定的封装一下:

#define ml_ssertTrue(expr) XCTAssertTrue((expr), @"设置指定的字符串,同下")
#define ml_assertFalse(expr) XCTAssertFalse((expr), @"")
#define ml_assertNil(a1) XCTAssertNil((a1), @"")
#define ml_assertNotNil(a1) XCTAssertNotNil((a1), @"")
#define ml_assertEqual(a1, a2) XCTAssertEqual((a1), (a2), @"")
#define ml_assertEqualObjects(a1, a2) XCTAssertEqualObjects((a1), (a2), @"")
#define ml_assertNotEqual(a1, a2) XCTAssertNotEqual((a1), (a2), @"")
#define ml_assertNotEqualObjects(a1, a2) XCTAssertNotEqualObjects((a1), (a2), @"")
#define ml_assertAccuracy(a1, a2, acc) XCTAssertEqualWithAccuracy((a1),(a2),(acc))

简单实用准备工作

795875-a207ce29f12fa7f2.png.jpeg

如果是之前创建的项目,里面没有勾选,可以自己创建:

795875-9e3ebd787a699757.png.jpeg

进入到这个类,

  • setUp是每个测试方法调用执行,
  • tearDown是每个测试方法调用执行。
  • testExample是测试方法,和我们新建的没有差别。
  • 不过测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法。测试方法的执行顺序是字典序排序。
  • 按快捷键Command + U进行单元测试,这个快捷键是全部测试。
795875-362afa43ea505bb6.png.jpeg

一、简单使用单元测试
1、创建一个Person类,里面有一个test1类方法
2、顺便勾选右边的测试单元按钮,为了对这个.m文件的数据测试

795875-192f3a273594f9e7.png.jpeg

3、如果项目比较小,就可以直接在项目刚创建的tests方法里测试,如果项目比较大的话,就创建一个单独的tests,例如:PersonTets:

795875-89721ddcf3d48789.png.jpeg

4、创建一个测试方法:testPerson:

795875-cbeeb6ba7a19c6ef.png.jpeg

上面也说了:测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法
如果前面不出现小菱形,可以Build一下

5、点击小菱形图标,出现绿色说明成功:

795875-fdefc42918aaccb4.png.jpeg

6、有时候需要验证值的话,就使用 XCTAssert(<#expression, ...#>),或者其他的断言方法测试结果;

二、对方法引用AFN框架的单元测试
1、测试带有网络请求的方法,例如:

795875-12be468144d6e31a.png.jpeg

2、 按照之前的那样 [Person test2]的话,点击小菱角会出现找到AFN框架:

795875-d4d646284ee3dd06.png.jpeg

3、需要做一些配置
3.1、复制Target(App) - Build Setting - Header Search Paths 的路径。

795875-27159a355bc9a14f.png.jpeg

3.2、粘贴到Target(UnitTests) - Build Setting - Header - Search Paths里。
3.3、复制Target(App) - Build Setting - User-Defined - PODS_ROOT整条。
3.4、到Target(UnitTests) - Build Setting - User-Defined新建一条PODS_ROOT。

795875-601bbddba0354ed1.png.jpeg

4、OK了!

三、期望
期望实际上是异步测试,当测试异步方法时,因为结果并不是立刻获得,所以我们可以设置一个期望,期望是有时间限定的的,fulfill表示满足期望。
例如

- (void)testAsynExample {

    // 1. 创建期望
    XCTestExpectation *exp = [self expectationWithDescription:@"这里可以是操作出错的原因描述。。。"];

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperationWithBlock:^{

        //模拟:这个异步操作需要2秒后才能获取结果,比如一个异步网络请求
        sleep(2);

        //模拟:获取的异步操作后,获取结果,判断异步方法的结果是否正确
        XCTAssertEqual(@"a", @"a");

        //2. 如果断言没问题,就调用fulfill宣布测试满足
        [exp fulfill];
    }];

    //3. 设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
        if (error) {
            NSLog(@"Timeout Error: %@", error);
        }
    }];
}

这个测试肯定是通过的,因为设置延迟为3秒,而异步操作2秒就除了一个正确的结果,并宣布了条件满足 [exp fulfill],但是当我们把延迟改成1秒,这个测试用例就不会成功,错误原因是 expectationWithDescription:@"这里可以是操作出错的原因描述。。。

异步测试除了使用expectationWithDescription
以外,还可以使用expectationForPredicate和expectationForNotification
,
掌握expectationWithDescription
即可,后两者者仅仅了解

下面这个例子使用expectationForPredicate 测试方法,代码来自于AFNetworking,用于测试backgroundImageForState方法

- (void)testThatBackgroundImageChanges {
    XCTAssertNil([self.button backgroundImageForState:UIControlStateNormal]);
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIButton  * _Nonnull button, NSDictionary<NSString *,id> * _Nullable bindings) {
        return [button backgroundImageForState:UIControlStateNormal] != nil;
    }];

    [self expectationForPredicate:predicate
              evaluatedWithObject:self.button
                          handler:nil];
    [self waitForExpectationsWithTimeout:20 handler:nil];
}

利用谓词计算,button是否正确的获得了backgroundImage,如果正确20秒内正确获得则通过测试,否则失败。

expectationForNotification 方法 ,该方法监听一个通知,如果在规定时间内正确收到通知则测试通过。

- (void)testAsynExample1 {
    [self expectationForNotification:(@"监听通知的名称xxx") object:nil handler:nil];
    [[NSNotificationCenter defaultCenter]postNotificationName:@"监听通知的名称xxx" object:nil];

    //设置延迟多少秒后,如果没有满足测试条件就报错
    [self waitForExpectationsWithTimeout:3 handler:nil];
}

这个例子也可以用expectationWithDescription实现,只是多些很多代码而已,但是这个可以帮助你更好的理解expectationForNotification 方法和 expectationWithDescription 的区别。同理,expectationForPredicate方法也可以使用expectationWithDescription实现。

func testAsynExample1() {
    let expectation = expectationWithDescription("监听通知的名称xxx")
    let sub = NSNotificationCenter.defaultCenter().addObserverForName("监听通知的名称xxx", object: nil, queue: nil) { (not) -> Void in
        expectation.fulfill()
    }
    NSNotificationCenter.defaultCenter().postNotificationName("监听通知的名称xxx", object: nil)
    waitForExpectationsWithTimeout(1, handler: nil)
    NSNotificationCenter.defaultCenter().removeObserver(sub)
}

四、期望实战:

-(void)testRequest{
    # 创建期望
    XCTestExpectation *expectation =[self expectationWithDescription:@"没有满足期望"];

    AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
    sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [sessionManager GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        NSLog(@"responseObject:%@", [NSJSONSerialization JSONObjectWithData:responseObject options:1 error:nil]);

        # 不为nil 通过,
        XCTAssertNotNil(responseObject, @"返回出错");

         # 满意
        [expectation fulfill];

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        # 为nil 通过,
        XCTAssertNil(error, @"请求出错");

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

推荐阅读更多精彩内容