我相信测试视图控制器是有意义的,但仍很困难。主要的困难应该是以下两个。
页面对象模式针对第一个。它封装围绕 UI 结构的复杂性,并有助于保持测试的整洁。
而不是像以下这样编写
UIButton * button = [viewController valueForKey:@"saveButton"];
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]
UILabel *statusLabel = [viewController valueForKey:@"statusLabel"];
NSString *statusText = statusLabel.text;
XCTAssertEqualObjects(@"Done", statusText);
页面对象允许测试看起来像以下这样
[object tapSaveButton];
NSString *statusText = [object statusText];
XCTAssertEqualObjects(@"Done", statusText);
您仍然必须在对象中实现复杂的但枯燥的视图操作。然而,现在的测试已经很好了;易于阅读和理解,对于小的更改工作得更好。
这个库还提供了一些与异步视图控制器事件一起工作的原始操作。
viewDidAppear:
和 viewDidDisappear:
waitFor:
以等待一些状态变化的完成,从而使您的测试更加稳定您可以通过 Cocoapods 安装 Obihiro。
pod 'Obihiro'
您的对象定义可能看起来像以下内容
#import <Obihiro/Obihiro.h>
#import "YourViewController.h"
@interface YourViewControllerObject : OBHViewControllerObject<YourViewController *>
@property (nonatomic, readonly) NSString *statusText;
- (void)tapSaveButton;
@end
@implementation YourViewControllerObject
- (NSString *)statusText {
// self.viewController is typed as YourViewController *.
// You can use methods in YourViewController without writing casts.
...
}
- (void)tapSaveButton {
// simulateUserAction: executes given block, and inserts wait for 100ms.
// This allows running next scheduled action.
[self simulateUserAction:^{
...
}]
}
@end
这对应于 Web 应用程序开发上下文中的页面对象。您的测试使用对象来访问视图控制器。
#import <XCTest/XCTest.h>
#import "YourViewControllerObject.h"
@interface YourViewControllerTests : XCTestCase
@property (nonatomic) YourViewControllerObject *object;
@end
@implementation YourViewControllerTests
- (void)setUp {
[super setUp];
self.object = [YourViewControllerObject objectWithInitialViewControllerFromStoryBoardWithName:@"MainStoryBoard"];
}
- (void)tearDown {
self.object = nil;
[super tearDown];
}
- (void)testSomething {
// It presents your View Controller, and returns after `viewDidAppear:` finished.
// Your View Controller would be completely initialized now.
[self.object presentViewController];
[self.object tapSaveButton];
NSString *status = self.object.statusText;
XCTAssertEqualObjects(@"Done", status);
}
@end
OBHViewControllerObject
提供了 registerObjectClass:
实例方法。它允许您的视图控制器对象实例化子视图控制器的自定义类。
@implementation YourViewControllerTests
// This is the method to be used initialization.
// You do not have to override `init` methods.
- (void)initializeObject {
[self.object registerObjectClass:[YourChildViewControllerObject class]];
// Now View Controller Objects instantiated thorough this object for YourChildViewController instances are instances of YourViewControllerObject.
// The class name of View Controller is guessed from the class name of View Controller Object, just dropping Object suffix.
}
@end
- (void)testSomething {
YourChildViewControllerObject *childObject = [self.object firstChildObjectForViewControllerClass:[YourChildViewController class]];
}
它提供了 UINavigationController
的默认对象类,这是最常用的视图控制器容器之一。
OBHNavigationControllerObject *navigationObject = [OBHNavigationControllerObject objectWithViewController:navigationViewController];
它提供了一些 API 用于视图控制器栈操作,例如获取与导航堆栈中最高视图控制器关联的对象。
YourViewControllerObject *object = [navigationObject topObjectOfViewControllerClass:[YourViewController class]];
[navigationObject back];
还有关于 UIBarButtonItems 的操作。
XCTAssertTrue(navigationObject.isRightButtonAvailable);
[navigationObject tapRightButton];
OBHViewControllerObject
允许访问由视图控制器弹出的弹出框。它假设弹出框是由 UIPopoverPresentedController
管理的,而不是 UIPopoverController
。
OBHViewControllerObject *object;
XCTAssertNotNil(object.presentedPopoverObject);
它还允许访问 UIAlertController
。
OBHViewControllerObject *object;
OBHAlertControllerObject *alertObject = object.alertObject;
XCTAssertEqualObjects(@["OK"], alertObject.titles);
[alertObject tapAlertButtonForTitles:@"OK"];
让你的测试稳定很难。让测试稳定的一种方式是使测试可重复。
而不是只测试一次UI状态
XCTAssertTrue(self.object.hasPopover);
试着写一个直到成功才结束的代码。
XCTAssertTrue([self.object eventually:^BOOL{
return self.object.hasPopover;
}]);
让每个谓词重试是个好主意,你可以定义一个类似这样的谓词方法
- (BOOL)hasPopover {
return [self eventuallyNotNil:^{
return self.presentedPopoverObject;
}];
}
这有助于保持测试的清洁。
// Now the test retries until success
XCTAssertTrue(self.object.hasPopover);
然而,否定与你预期的相反。
// Write test script to close popover
// This may not work
XCTAssertFalse(self.object.hasPopover);
你想要的测试实际上是这个样子的
XCTAssertTrue([self.object eventuallyNil:^{
return self.object.presentedPopoverObject;
}]);
你心中测试的阳性形式,《气泡弹出》,意味着
你心中的测试否定,《气泡没有弹出》,意味着
Q
不是 P
的否定。你必须定义另一个谓词
- (BOOL)doesntHavePopover {
return [self eventuallyNil:^{
return self.presentedPopoverObject;
}];
}
以下测试将正常工作。
// Write test script to close popover
// This may not work
XCTAssertTrue(self.object.doesntHavePopover);
为所有谓词定义正负形式一定很无聊。
Obihiro 1.2介绍了 OBHUIPredicate
。
- (OBHUIPredicate *)popoverPresented {
return [self predicateWithTest:^{
return self.object.presentedPopoverObject != nil;
}];
}
这就像现代测试框架(包括Rspec)中的匹配器的概念。你可以在测试中使用它。
// Positive form
XCTAssert(self.object.popoverPresented.holds);
// Negative form
XCTAssert(self.object.popoverPresented.doesntHold);
XCTAssert(self.object.popoverPresented.negation.hold); // Another syntax
使用 OBHUIPredicate
来定义UI测试,并保持测试代码的简洁。
有两种(或更多)方式来模拟用户操作
我推荐使用KIF,但某些动作仍然难以模拟。为每个你想要模拟的动作选择更好的方式。
如果你找不到任何模拟用户操作的方法,只需访问你的视图控制器内部结构。这并不是一个好方法,但比停止编写测试或暴露内部结构到测试脚本中要好。
如果你要自己写,像下面这样的方法会有效。
UIButton *button;
[self simulateUserAction:^{
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
}];
在KIF中,你可以使用tap
方法。
UIButton *button;
[self simulateUserAction:^{
[button tap];
}];
这看起来没有太大区别。
我没有找到KIF的方式。
UIBarButtonItem *button;
[self simulateUserAction:^{
[button.target performSelector:button.action withObject:button afterDelay:0];
}];
看起来并不理想,但奏效。
这是KIF做得更好的动作。
UITableViewCell *cell;
[self simulateUserAction:^{
[cell tap];
}];
对于UITableViewDelegate cells来说,这并不是很难。
UITableView *tableView;
NSindexPath *indexPath;
UITableViewCell *cell;
[self simulateUserAction:^{
[self.tableView.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
}];
然而,如果单元格是为 segue 定制的,通过公开的 API 没有类似的方式来模拟。
UITableView *tableView;
NSindexPath *indexPath;
UITableViewCell *cell;
[self simulateUserAction:^{
id<NSObject> template = [cell performSelector:NSSelectorFromString(@"selectionSegueTemplate")];
if (template) {
[template performSelector:NSSelectorFromString(@"perform:") withObject:cell];
return;
}
}];
这取决于。我个人更喜欢自己动手做。但KIF会提供更好的模拟(我不确定,我不想完全设置KIF)。
UITextField *textField;
[self simulateUserAction:^{
[field sendActionsForControlEvents:UIControlEventEditingDidBegin];
textField.text = text;
[field sendActionsForControlEvents:UIControlEventEditingDidEnd];
}];