Obihiro 1.3.2

Obihiro 1.3.2

测试已测试
语言语言 Obj-CObjective C
许可证 MIT
发布最后发布2018年1月

Soutaro Matsumoto 维护。



Obihiro 1.3.2

  • Soutaro Matsumoto

Obihiro - 视图控制器页面对象

我相信测试视图控制器是有意义的,但仍很困难。主要的困难应该是以下两个。

  1. UI 变化很大,测试很容易过时
  2. 视图控制器生命周期高度异步

页面对象模式针对第一个。它封装围绕 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

它提供了 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;
}]);

你心中测试的阳性形式,《气泡弹出》,意味着

  • (P)在短时间内,气泡会弹出

你心中的测试否定,《气泡没有弹出》,意味着

  • (Q)在短时间内,气泡不会弹出

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测试,并保持测试代码的简洁。

模拟用户操作

有两种(或更多)方式来模拟用户操作

  1. 编写你的代码
  2. 使用KIF https://github.com/KIF/KIF

我推荐使用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]; 
}];

看起来并不理想,但奏效。

选择UITableViewCell

这是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];
}];