iOS--单元测试和UI测试教程

最近,上线的版本中改动了一个接口,当时没有注意导致了线上严重的Crash.很难受。
哎,不得的不好好学习一下单元测试了。
大家一起学习,一起进步。
大体下面东西分2块。
1.教程
这部分 我也是翻译看了国外网站的文章
https://www.raywenderlich.com/150073/ios-unit-testing-and-ui-testing-tutorial
没有照着上面翻译,完全根据心情来。如果同学,看不下去。可取直接去看原文
自备梯子,语言环境Swift3.0
2 实践
这部分我会抽取一些开源库的测试和我实际项目中的测试来展示

手把手的教程

开始

首先下载教程项目项目 BullsEye 和HalfTunes
BullsEye 是一个模拟靶心的游戏
HalfTunes是从itunes搜索歌曲的项目

在Xcode中单元测试

我们首先打开BullsEye项目,xocde左边导航栏第5个就是测试的栏目,我们可以通过commond+5快速选择。

7DA041BB-4040-4F72-AEFF-0580E1F99EF7.png

首先我们在新建项目的时候有选项让我们是否选择创建单元测试,如果你已经选中了,这部完全可以跳过。 教程给的项目是没有预先创建的。

0CF65EE6-1CC2-4575-B179-98D5A741FE64.png

按照第一张图,我们可以创建测试的模板。
我们在系统生成的类中能获取的信息就是
测试的父类 : XCTestCase
开始的方法: setup()
测试结束走的方法:teardown()

3种运行测试的方法
1.运行全部测试用例 Product\Test 或者 Command-U
2 和 3 看图

B3CC2363-3F09-455B-9F26-948B9D79DC36.png

点击上面的按钮符号 就可以 运行 该类下的所有或者个别方法的测试用例

当运行成功的时候 会变成balabala(你还是自己看图吧)

9EAB8195-E2F0-4194-B81A-613B46DE5BC3.png

在运行完测试用例的时候在 testPerformanceExample()方法中有个测试时间统计 你点击 会出现上图出现的效果

使用XCTAssert 去测试models

在这个进行之前,我建议同一门去看下项目的代码 熟悉 代码,这样事半功倍。
打开BullsEyeTests.swift文件

D9B4979B-6FB1-499B-AA65-07A2C647ED3F.png

添加上图一样的代码
首先gameUnderTest是一个类级别的SUT(System Under Test)对象,很多测试都会根据这个SUT对象进行

这边作者强调要在teardown()中的方法中释放SUT对象

D8D9A35F-B5C2-49B4-8F5A-DA74B5BAF884.png

一个测试方法的名字必须是test开头,测试的时候遵循以下几个描述
1.在given中,设置所有需要的值:在例子中 你创建了一个guess值,所以你可以明确与targetValue的差值。

2在when中,执行测试的代码:调用gameUnderTest.check(_:)

3在then中,断言你期望的结果(在项目的例子中,gameUnderTest.scoreRound是100-5 )和带有失败的结果信息

运行测试,如果成功 测试标记会变成绿色

BF06B2A4-675F-4509-B988-E98D8831855F.png

调试一个测试用例

我们平时debug的时候都会打断点,在测试的时候会有点区别 看下图

6C1245B8-E5BF-4D72-9D6B-6A8740409013.png

如果我们在 given 的时候给了错误的 预给值,就会直接到断言这一步

AF9091FD-D27E-47B1-A908-D17FEF1EAC9C.png

这地方测试model的时候,我们一半都用来测试数据的正确性

使用 XCTestExpectation 来测试异步操作

我们打开第二个教程项目HalfTunes
当然还是单元测试,上面我们都讲过了怎么新建,这边就不说了。
1.首先我们引入需要测试的项目

@testable import HalfTunes
  1. 在setup 和 teardown中 创建和释放SUT对象
var sessionUnderTest: URLSession!
override func setUp() {
  super.setUp()
  sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default)
} 
override func tearDown() {
  sessionUnderTest = nil
  super.tearDown()
}

3.添加异步测试用例

// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
  // given
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Status code: 200")
 
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
}

这个测试用例会发送一个有效的查询到itunes并且返回一个200的的状态码。和平时写的网络请求差不多,只是添加了以下几行代码
1.expectation(_:)返回一个XCTestExpectation对象。一般接受变量可以这样命名promise expectation future上面用了promise。description参数,是你期望得到的描述。

2 当在闭包中得到你想要的结果就可以调用promise.fulfill()

3 waitForExpectations(_:handler:)这个方法会让测试持续的运行,知道所有的 expectation 都被fulfill() 或者到达超时时间。

===
上面的方法是进行网络请求,如果网络请求成功的话。一点问题都没有。但是如果网络请求不成功的话,就意味着你没有调用promise.fulfill()。这样的后果就是: 一直等待超时

所谓为了更好的完成测试需要代码改进成这样

// Asynchronous test: faster fail
func testCallToiTunesCompletes() {
  // given
  let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?
 
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    // 2
    promise.fulfill()
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
 
  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

只要闭包回调就直接调用promise.fulfill() 之后再用断言在后面判断

模拟对象和交互

异步测试会让确认你给一步api一个正确的输入,获取你想测试网络请求返回之后代码是否工作正常,或者是否正常更新了数据库。

大多的app都会和系统的库进行交互,这些对象我们不能控制而且特使这些对象的交互会非常的慢以及不可重复。这是你就可以通过输入存根或假的交互通过更新模拟对象。
这边说的不是很好理解,具体看代码

进行虚拟的输入

在下面的测试用例中,你会检查updateSearchResults(_:)是否正确的解析了网络返回的数据。SUT是VC而且你要虚拟网络回话以及准备好预下载的数据。

var controllerUnderTest: SearchViewController!
 
override func setUp() {
  super.setUp()
  controllerUnderTest = UIStoryboard(name: "Main", 
      bundle: nil).instantiateInitialViewController() as! SearchViewController!
}
 
override func tearDown() {
  controllerUnderTest = nil
  super.tearDown()
}

这边SUT是VC 是因为SearchViewController.swift是一个臃肿的VC,如果将网络模块移到单独的地方,就会坚守这些问题。让测试更简单。

模拟网络请求: DHURLSessionMock.swift 已经定义好了
模拟数据:https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3 自己下

然后在setup()

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)
 
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)
 
let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)
controllerUnderTest.defaultSession = sessionMock

测试用例

// Fake URLSession with DHURLSession protocol and stubs
func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")
 
  // when
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) {
    data, response, error in
    // if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks
    if let error = error {
      print(error.localizedDescription)
    } else if let httpResponse = response as? HTTPURLResponse {
      if httpResponse.statusCode == 200 {
        promise.fulfill()
        self.controllerUnderTest?.updateSearchResults(data)
      }
    }
  }
  dataTask?.resume()
  waitForExpectations(timeout: 5, handler: nil)
 
  // then
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

当我们准备的假数据进行解析的时候完成的时候,我们用断言来判断searchResults.count是否为3 来判断是否解析成功。

UI测试

Xcode7开始介绍了UI测试,让你通过记录与UI的交互来创建一个测试用例。UI测试作用通过寻找一个UI对象的查询和异步事件,然后发送他们给这些对象。api让你可以检查UI对象和状态去比较是否与你期望的状态不同。

我们打开BullsEye项目

我们在在选择 slider和type的时候上面文本和滑动条会有不同的状态。
我们下面的测试用例就是确保选择不同类型的时候 文本和滑动条处于正确的状态。

1 像单元测试一样新建一个UI测试的target

2
声明属性

var app: XCUIApplication!

将setup方法中的XCUIApplication().launch()替换

app = XCUIApplication()
app.launch()

3 写测试用例
测试用例怎么写 ,这个我们可以用


29367269-0564-4C38-939C-6C4CD235094E.png

来记录我们的UI操作
假如我们点击了滑动条和上面的文本,就会自动生成操作代码

let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

app 我们已经声明成全局的 所以第一行去掉。
第 2 3 行记录我们的点击事件,可是我们只需要滑动条和文本对象。去掉tap()

然后 我们第一步given就完成了

// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

when and then
下面的代码也很好理解,这样我们就完成了一个简单的UI测试

// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
 
  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
 
  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

性能测试

性能测试很简单


9E1B1F5C-44EC-4EF9-8926-3158E9FDC0DB.png

只要将测试代码放进去
就可以看到性能分析

4DF2908A-01A8-4B8B-A0E9-D535C130A19D.png

测试覆盖率

按照下面的图一步一步
1
设置


C6F8B796-99D0-4E2E-A9CE-C0510A1ED342.png

2
查看测试率

AB639919-9699-49CB-A622-D4B8981B36A7.png

3
文件中绿色代表测试 红色代表没测试


2A2DFCCC-5FBB-48CE-AE1D-04C29FC874C1.png

实践

我们来来看MB的测试用例
从hud的创建和隐藏 每一步都会根据断言去判断

- (void)testNonAnimatedConvenienceHUDPresentation {
    UIViewController *rootViewController = UIApplication.sharedApplication.keyWindow.rootViewController;
    UIView *rootView = rootViewController.view;

    //获得一个hud
    MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:rootView animated:NO];

    //测试是否创建成功
    XCTAssertNotNil(hud, @"A HUD should be created.");

    //测试hud 是否可见
    XCTAssertEqualObjects(hud.superview, rootView, @"The hud should be added to the view."); \
    XCTAssertEqual(hud.alpha, 1.f, @"The HUD should be visible."); \
    XCTAssertFalse(hud.hidden, @"The HUD should be visible."); \
    XCTAssertEqual(hud.bezelView.alpha, 1.f, @"The HUD should be visible.");

    XCTAssertFalse([hud.bezelView.layer.animationKeys containsObject:@"opacity"], @"The opacity should NOT be animated.");

    XCTAssertEqualObjects([MBProgressHUD HUDForView:rootView], hud, @"The HUD should be found via the convenience operation.");

    XCTAssertTrue([MBProgressHUD hideHUDForView:rootView animated:NO], @"The HUD should be found and removed.");

    MBTestHUDIsHidenAndRemoved(hud, rootView);

    XCTAssertFalse([MBProgressHUD hideHUDForView:rootView animated:NO], @"A subsequent HUD hide operation should fail.");
}

Alamofire
简单的网络请求测试

class RequestInitializationTestCase: BaseTestCase {
    func testRequestClassMethodWithMethodAndURL() {
        // Given
        let urlString = "https://httpbin.org/"

        // When
        let request = Alamofire.request(urlString)

        // Then
        XCTAssertNotNil(request.request)
        XCTAssertEqual(request.request?.httpMethod, "GET")
        XCTAssertEqual(request.request?.url?.absoluteString, urlString)
        XCTAssertNil(request.response)
    }

我自己项目里的登录接口测试

- (void)testLogin {
    // This is an example of a functional test case.
    //1 given
    NSString *phone = @"XXXXX";
    NSString *password = @"XXXXXX";

    __block NSDictionary * reponse ;

     XCTestExpectation *promise = [[XCTestExpectation alloc] init];

    //when
    [IGONetworkingManager requestUserLoginWithPhone:phone password:password view:nil response:^(id data, NSError *error) {
        reponse = (NSDictionary *)data;
        [promise fulfill];

    }];
    [self waitForExpectationsWithTimeout:8 handler:nil];
    //then
    XCTAssertNil(reponse);
    XCTAssertEqual(reponse[@"code"], @"1");

    //==数据返回成功,解析
    // given
    User *user = [User currentUser];
    // then
    XCTAssertNil(user.userID);
    XCTAssertNil(user.userToken);
    XCTAssertNil(user.regID);

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

推荐阅读更多精彩内容