yinxing29

iOS Unit Test And UI Test

2019/03/21 Share

Unit Test 和 UI Test

[TOC]

Unit Test

单元测试:从字面上的意思理解就是,将某一部分的代码拆成一块一块的代码进行测试,每一块代码就是一个独立的单元。一般用于测试某一块代码的逻辑是否正确。

  • Unit Test文件

    在每个工程创建的时候,都会有3个勾选项供你勾选,其中两个默认选中的 Include Unit TestsInclude UI Tests,勾选后就会在工程文件中生成两个文件夹ProjectNameTestsProjectNameUITestsProjectNameTests就是用于编写这个工程的单元测试的地方,而ProjectNameUITests就是用于编写UI 自动化测试的地方。也可以通过file -> New -> Target -> iOS -> 搜索test,添加iOS Unit Testing BundleiOS UI Testing Bundle添加。我们先来看一下Unit Test

    我们可以在ProjectNameTests分组中创建对应功能模块的测试文件,系统会自动创建几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//所有测试用例运行之前都会运行的方法,在该测试文件中的各个测试单元都会用到的一些实体类,都可以在该方法中init
- (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];
}
//测试用例编写用例,在编写测试单元的时候,所有方法都必须以“test”开头,并却不能带任何参数
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
//用于性能测试,比如for循环的耗时测试等。
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
  • 主要用途:

    1. 当修改某个公共类的时候,为防止对其他地方造成影响,需要验证一下其他地方是否正确,可以使用单元测试验证,而不需要一步一步的去实现复杂的操作。
    2. 在开发的时候,有时候需要反复调试接口返回的数据情况或者验证某段代码的正确性,可能要一遍一遍的运行,而每次运行后都需要点击到当前页面,这样经常需要重复复杂的操作,而Unit Test则可以省略那些复杂操作。
  • 异步操作

    Unit Test的每一个测试单元都可以看做是一个串行线程的程序,当你的测试单元涉及到异步的时候,就需要用到XCTestExpectation,它表示你的一个“期望”,在规定的时间内,当你的“期望”完成时,才会结束这个测试单元。实例如下:

1
2
3
4
5
6
7
8
XCTestExpectation *expectation = [self expectationWithDescription:@"网络请求"];
[self.viewModel requestSuccessBlock:^(id responseObject){
[expectation fulfill];
} failedBlock:^(NSError *error){
[expectation fulfill];
}];
//在10s内还没有达到期望的时候,就会提示错误
[self waitForExpectationsWithTimeout:10.0 handler:nil];
  • 常用的断言测试语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
XCTFail(format…)  //生成一个失败的测试;
XCTAssertNil(a1, format...) //a1 = nil,测试通过
XCTAssertNotNil(a1, format…) //a1 != nil,测试通过
XCTAssert(expression, format...) //传入的expression结果为 true,测试通过;
XCTAssertTrue(expression, format...) //同上
XCTAssertFalse(expression, format...) //传入的expression结果为false,测试通过;
XCTAssertEqualObjects(a1, a2, format...) //[对象a1 isEqual:对象a2]为true,测试通过
XCTAssertNotEqualObjects(a1, a2, format...) //[对象a1 isEqual:对象a2]为false,测试通过
XCTAssertEqual(a1, a2, format...) //a1 = a2(a1,a2可以是整型,浮点型,结构体,联合体),测试通过;
XCTAssertNotEqual(a1, a2, format...) //a1 != a2(a1,a2可以是整型,浮点型,结构体,联合体),测试通过;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...) //|a1 - a2| = accuracy,测试通过
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) //|a1 - a2| != accuracy,测试通过
XCTAssertThrows(expression, format...) //异常测试,当expression发生异常时通过;反之不通过;
XCTAssertThrowsSpecific(expression, specificException, format...) //异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...) //异常测试,当expression发生具体异常、具体异常名称的异常时通过测试;
XCTAssertNoThrow(expression, format…) //异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrowSpecific(expression, specificException, format...) //异常测试,当expression没有发生特定异常时,通过测试;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...) //异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试。

/**
format可以设置断言不通过时的提示语,同时会将传入的参数展示出来。有时候为了结果更加一目了然,可以使用相反的断言语法,提示正确的内容。
*/
  • Unit Test注意事项

    Unit Test是一个target,所以在Unit Test中导入类的时候,如果涉及到prefix header中的类或者通过cocoaPods导入的三方库的时候,需要在对应的target中需要设置prefix header path和在Podfile文件中添加对应的target

UI Test

UI自动化测试:通过计算机语言模拟用户操作。在iOS 3.0的时候,苹果引入了UI Accessibility,用来辅助身体不方便的人来使用app,与VoiceOver相互配合,用来阅读被UI Accessibility分类和标记的UI元素操作app。而UI Test就是基于UI Accessibility的,所以我们在写UI Test时,其实就是通过对应的API来查询到UI上面的元素,并模拟点击等事件。为了方便开发者编写测试代码,xCode有一个功能叫录屏,当你开始录制的时候,他会将你的一系列操作自动生成代码,但是要想后面正常跑起来,还需要自己去修改,所以还是需要你去了解UI Test相应的API。
image

UI Test中我们使用到的类主要有XCUIApplicationXCUIElementQueryXCUIElement

XCUIApplication

  • 继承自XCUElement,类似于AppDelegate,管理应用程序的生命周期,在写测试代码的时候,可以说最重要的一句代码就是XCUIApplication *app = [[XCUIApplication alloc] init];,后续获取界面的元素都需要通过app来获取。

  • 属性:

    1. launchArguments:将在启动时传递给应用程序的参数。
    2. launchEnvironment:将在启动时传递给应用程序的环境变量。
    3. state:只读。应用程序的状态。
  • 方法:

    1. -launch:启动程序。
    2. -terminate:终止程序。
    3. -activate:激活应用程序。
    4. -(BOOL)waitForState:(XCUIApplicationState)state timeout:(NSTimeInterval)timeout:等待应用程序进入某种状态,几秒钟后失效,如果应用程序当前处于指定状态或转换到所需状态,则返回YES,继续执行下面的代码。

创建UI Test文件,系统自动生成的方法和Unit Test是差不多的,添加测试方法的规则也一样,test开头,不能传参

XCUIElementQuery

查询元素,可以说是所有UI元素的一个容器。

  • 属性:
    1. element:query只有一个元素的时候,query就是element,可以执行点击等操作。
    2. count:query中元素的数量。
    3. allElementsBoundByAccessibilityElement:得到query中所有的XCUIElement元素。
    4. allElementsBoundByIndex:得到query中所有的XCUIElement元素。
    5. debugDescription:提供有关查询的调试信息。
  • 方法:

    1. -elementAtIndex:通过下标来获取对应的元素,返回一个XCUIElement,在iOS9被弃用,可以使用-elementBoundByIndex
    2. -elementBoundByIndex:通过下标来获取query中的元素,返回一个XCUIElement
    3. -elementMatchingPredicate:通过匹配NSPredicate(如:placeholderValue == @"Type in number")来获取元素,返回一个XCUIElement
    4. -elementMatchingType:identifier:通过类型(如:XCUIElementTypeTextField)和id(可以是UIButtontitle)来获取一个元素,返回一个XCUIElement
    5. -objectForKeyedSubscript:返回一个由键指定的标识符的子代元素,返回一个XCUIElement???
    6. -descendantsMatchingType:传入一个元素类型,返回一个新的XCUIElementQuery,查询对象为当前控件的子子孙孙元素。
    7. -childrenMatchingType:传入一个元素类型,返回一个新的XCUIElementQuery,查询对象为当前控件的子元素。
    8. -matchingPredicate:传入NSPredicate,过滤元素,得到对应的XCUIElementQuery
    9. -matchingType:identifier:通过类型和id来获取一个新的XCUIElementQuery
    10. -matchingIdentifier:通过id得到一个新的XCUIElementQuery
    11. -containingPredicate:通过NSPredicate,从子节点中找到包含该条件的XCUIElementQuery对象。
    12. -containingType:identifier:通过类型和id从子节点中找到包含该条件的XCUIElementQuery对象。

      事例:

1
2


XCUIElement

  • XCTest.framework中代表所有UI控件的抽象类,通过这个类来实现点击滑动等操作。
  • 属性:
    1. exists:判断元素是否存在。
    2. hittable:确定是否可以为元素计算生命值。
    3. debugDescription:提供有关该元素的调试信息。
  • 方法:

    1. -waitForExistenceWithTimeout:等待元素的exist属性为真所需的指定时间量,如果超时过期而元素不存在,则返回false。
    2. -descendantsMatchingType:返回与指定类型匹配的元素的所有后代的查询。
    3. -childrenMatchingType:返回与指定类型匹配的元素的所有直接子节点的查询。
    4. -coordinateWithNormalizedOffset:创建并返回带有标准化偏移量的新坐标。
    5. -typeText:输入文本。
    6. -tap:点击。
    7. -doubleTap:双击。
    8. -twoFingerTap:双指点击。
    9. -tapWithNumberOfTaps:numberOfTouches使用一个或多个触点发送一个或多个轻击。
    10. -pressForDuration:长按指定时间。
    11. -pressForDuration:thenDragToElement:拖动到指定元素
    12. -swipeUp:上滑。
    13. -swipeDown:下滑。
    14. -swipeLeft:左滑。
    15. -swipeRight:右滑。
    16. -pinchWithScale:velocity:捏合手势。
    17. -rotate:withVelocity:旋转手势。
    18. -adjustToPickerWheelValue:修改pickerView的选择值

    事例:

1
2


  1. Unit Test中,只需要注意一下异步操作
  2. UI Test中,第一次写测试代码,就需要注意的很多。
    • 通过录制自动生成的代码中,中文会转成Unicode,需要手动修改。
    • UI Test运行时,其实就是自动操作app,而有的界面是需要等待网络请求结束后才刷新或者添加控件的,但是测试代码却不会真实的等待请求结束,所以当去app查询某个元素点击的时候,会导致测试代码崩溃。这个时候,就需要我们去等待页面绘制或者控件添加出来后才执行点击等操作。解决方法见下面代码块一。
    • 在录屏过程中,点击cell时,报错:Timestamped Event Matchng Error: Failed To Find Matching Element,开始的时候一脸懵逼,后来一次不小心点到一个UILabel发现居然又可以了,后来查阅资料,发现UI控件有一个属性isAccessibilityElement,这个属性就是用来表示该控件是否可以被查询。而UIView的为NO,所以无法找到Element。需要点击cell上的UILabel等可以被访问的控件,或者获取UITableView上所有cell,然后通过下标去获取cell元素,然后实现点击操作。解决方法见下面代码块二。
    • 在点击一个别人封装的控件的时候,发现也是无法获取Element,然后点击进去看那个控件,发现是继承自UIButton,因为UIButtonisAccessibilityElement默认为YES,并且没有设置title或者image,所以,导致无法查询到该元素,和该控件上其他的元素。这个时候就需要在封装的时候,再其初始化方法中给该控件设置一个accessibilityIdentifier,并且将isAccessibilityElement设置为NO
    • 通过XCUIElementswipeUp方法滑动,无法滑到指定位置。解决方法见下面代码块三。
    • 有的界面,在请求到数据之前,就会将界面布局并显示出来,然后根据请求到的数据,隐藏获取添加其他控件,这个时候,如果在网络请求结束之前去查询某个元素,并tap,测试程序会崩溃。你看控制台的打印消息,就会告诉你元素偏移了。其实就是因为网络请求前后元素 位置发生了变化。所以这个时候,你需要等待网络请求结束再去查询这个元素。(我这里是通过网络请求是的菊花来判断网络请求是否结束)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 
等待页面绘制完成
*/
//1. 通过sleep使其等待一定时间。一定时间之后才执行后面的方法
sleep(10);
/*2. 这里因为这个弹窗可能走[alert.buttons[@"确定"] tap]的时候alert未弹出而获取失败,导致测试代码崩溃。
所以这里给了一个等待,在20sm内,测试程序会每隔1s去验证title为@"重要提示"的alert是否找到,如果找到,就立即继续运行后面的代码。
如果在20s内没有找到的话,就会报错,中断执行,提示超时未实现该等待。在这里NSPredicate的条件可以根据具体需求修改为其他的。*/
XCUIElement *alert = app.alerts[@"重要提示"];
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"exists = 1"]
evaluatedWithObject:alert
handler:nil];
//等待20s内,上述条件满足后才继续下面的内容(不一定等待20s)
[self waitForExpectationsWithTimeout:20.0 handler:nil];
[alert.buttons[@"确定"] tap];
//3.第三种方式和第二种方式差不多,也是在20s内每隔1s去判断alert是否存在。
XCUIElement *alert = app.alerts[@"重要提示"];
BOOL alertShow = [alert waitForExistenceWithTimeout:20.0];
1
2
3
4
5
6
7
8
9
/** 
获取界面上的 元素
*/

//获取所有的cells,并点击第一个cell。(如果一个页面有多个tableView,该方法可能导致获取的cell不是你想要的,可以通过方法二获取cell上面的label实现点击(label.text需要保证唯一))。
// 获取cell类的 query ,然后通过下标获取 element
XCUIElement *cell = [app.tables.cells elementBoundByIndex:0];
// 通过获取cell上的statiText类的 query,然后通过具体text获取 elment
XCUIElement *label = app.tables.cells.staticTexts[@"我是element"]; //他会去cell上查找text为@"我是element"的label,然后点击label达到点击cell的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 
滑动到指定位置
*/
//滑动到指定位置,通过 XCUIElement 的 -pressForDuration:thenDragToElement:拖动到 @"picker element" 元素
XCUIElement *firstCell = [app.tables.cells elementBoundByIndex:0];
XCUIElement *otherCell = [app.tables.cells elementBoundByIndex:4];
[otherCell pressForDuration:0.5 thenDragToElement:firstCell];


//下拉刷新,通过设置的偏移坐标,滑动到对应位置。
XCUIElement *pickerWheel = app.pickerWheels[@"picker element"];
XCUICoordinate *start = [pickerWheel coordinateWithNormalizedOffset:CGVectorMake(0, 0)];
XCUICoordinate *end = [pickerWheel coordinateWithNormalizedOffset:CGVectorMake(0, 0.2)];
[end pressForDuration:0.5 thenDragToCoordinate:start];

以上就是我在开发中遇到的一些坑,在遇到这些坑的时候,一脸懵逼,也在网上找了很久,就是没有找到解决方法,这些坑基本都是在毫无办法的时候,然后不小心点击到某个元素,获取在某种不同的数据情况下想到的原因。希望可以帮到大家。

原文作者: yinxing29

原文链接: http://yinxing29.github.io/2019/03/21/iOS - Unit Test 和 UI Test/

发表日期: March 21st 2019, 11:49:25

版权声明: 未经同意,禁止转载

CATALOG
  1. 1. Unit Test 和 UI Test
    1. 1.1. Unit Test
    2. 1.2. UI Test
      1. 1.2.1. XCUIApplication
      2. 1.2.2. XCUIElementQuery
      3. 1.2.3. XCUIElement
    3. 1.3.