视图控制器测试驱动开发

View Controller TDD

我正在尝试向我的项目添加一些单元测试以测试视图控制器。但是,我似乎在处理看似简单的事情时遇到了问题。我已经创建了一个示例项目,我将参考它。 https://github.com/pangers/ViewControllerTesting

示例包含一个 UINavigationController 作为初始视图控制器。 UINavigationController 的根视图控制器是 FirstViewController。 FirstViewController 上有一个按钮可以转到 SecondViewController。在 SecondViewController 中有一个空的文本字段。

我要添加的两个测试是:
1) 在 FirstViewController 中检查按钮标题是 "Next Screen".
2) 检查 SecondViewController 中的文本字段是否为空,"".

我听说将您的 swift 文件同时添加到主要目标和测试目标不是一个好的做法。但是最好在测试中制作任何你想访问的内容 public 并将主要目标导入到测试中。这就是我所做的。 (我还将主要目标的 "Defines Module" 设置为 YES,因为这也是我在几篇文章中读到的)。

在 FirstViewControllerTests 中,我用以下内容实例化了第一个视图控制器:

var viewController: FirstViewController!

override func setUp() {
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
    let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
    viewController = navigationController.topViewController as FirstViewController
    viewController.viewDidLoad()
}

并且我添加了测试:

func testCheckButtonHasTextNextScreen() {
    XCTAssertEqual(viewController.button.currentTitle!, "Next Screen", "Button should say Next Screen")
}

同样,对于 SecondViewControllerTest,我使用以下方法进行设置:

var secondViewController:SecondViewController!

override func setUp() {
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
    let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
    let firstviewController = navigationController.topViewController as FirstViewController
    firstviewController.performSegueWithIdentifier("FirstToSecond", sender: nil)
    secondViewController = navigationController.topViewController as SecondViewController
    secondViewController.viewDidLoad()
}

测试:

func testTextFieldIsBlank() {
    XCTAssertEqual(secondViewController.textField.text, "", "Nothing in textfield")
}

他们都失败了,我不太确定为什么。我怀疑我实例化视图控制器的方式不正确。实例化视图控制器的最佳方法是使用故事板(就像在现实生活中 运行 一样)吗?或者是否可以通过以下方式实例化:

var viewController = FirstViewController()

你们在 swift 中使用 TDD 和视图控制器有什么经验?

我正在使用 Swift 和 XCode 6.1.1.

提前致谢。

已解决

好的,在考虑了 modocache 和 Mike Taverne 的回答后,我找到了我的解决方案,并且学到了一些我将在下面写下来的东西。

1) 我做了任何我想测试的东西 class/method/variable public。我不需要将 swift 文件添加到测试目标。

2) 我只需要为 "Main" 目标设置 "Defines Module"(而不是 "Test" 目标或整个项目)

3) 实例化故事板时,bundle 应该设置为 nil 而不是 NSBundle(forClass: self.dynamicType),否则测试会失败。

4) 正如 modocache 所述,最好给你的视图控制器一个 StoryboardID 并像这样实例化它们:

viewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as FirstViewController

但是,像这样实例化视图控制器只会实例化视图控制器,而不实例化它可能嵌入的任何导航控制器。这意味着,尝试做

XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "Bar should show by default")

将导致 nil 异常。我用

确认了这一点
XCTAssertNil(viewController.navigationController?, "navigation controller doesn't exist")

测试成功。

因为我想在 FirstViewController 中检查导航栏的状态,所以您必须像这样实例化视图控制器:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as FirstViewController

正在执行测试

XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "nav bar should be showing by default")

测试成功。

5) let _ = viewController.view 确实会触发 viewDidLoad() ,这已被测试证实

6) let _ = viewController.view 不会触发 viewWillAppear(),之后我也可以推测。 viewController.viewWillAppear(false/true)需要手动调用才能触发(测试确认)

希望对大家有所帮助。如果有人想玩它,我会将更新后的项目推送到 GitHub(上面的 link)。

更新#2

经过以上所有,我仍然无法弄清楚如何从第一个视图控制器转换到第二个视图控制器(以便我可以测试 SecondViewControllerTests.swift 中的导航栏属性)。我试过了

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.performSegueWithIdentifier("FirstToSecond", sender: nil)
secondVC = nc.topViewController as SecondViewController

导致错误。

我也试过了

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.toSecondVCButton.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
secondVC = nc.topViewController as SecondViewController

没用。

我终于试过了

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
vc = storyboard.instantiateViewControllerWithIdentifier("Second") as SecondViewController
nc.pushViewController(vc, animated: false)
let _ = vc.view
vc.viewWillAppear(false)

这与我的测试完美配合(允许我访问导航栏属性)!

我最近开始对视图控制器进行单元测试,这带来了一些独特的挑战。

一个挑战是加载视图。查看 FirstViewController 的设置,您正在尝试使用 viewController.viewDidLoad().

我的建议是将该行替换为:

设虚拟=viewController.view

访问 .view 属性 将强制加载视图。这将在您的 ViewController 中触发 .viewDidLoad,所以不要在您的测试中显式调用该方法。

这种方法被一些人认为是 hacky,但它简单有效。 (参见 Clean way to force view to load subviews early

顺便说一句,我发现测试视图控制器的最佳方法是将尽可能多的代码从视图控制器移出到其他更容易测试的 classes 中。

如果您的视图控制器是在情节提要中定义的,那么您需要以这种方式实例化它,以便正确设置您的插座。尝试像普通 class 一样初始化它是行不通的。

我同意@MikeTaverne 的回答:我更喜欢访问 -[UIViewController view] 以触发 -[UIViewController viewDidLoad],而不是直接调用它。看看 FirstViewController 的测试失败是否会在您改用此方法后消失:

viewController = navigationController.topViewController as FirstViewController
let _  = viewController.view

我还建议在故事板中为两个视图控制器提供标识符。这将允许您直接实例化它们,而无需通过 UINavigationController:

访问它们
var secondViewController: SecondViewController!

override func setUp() {
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
    secondViewController = storyboard.instantiateViewControllerWithIdentifier("SecondViewController")
        as SecondViewController
    let _ = secondViewController.view
}

查看我在布鲁克林 Swift 关于测试 UIViewController 的演讲了解详情:https://vimeo.com/115671189#t=37m50s(我的演讲从 37'50" 开始)。