视图控制器 TDD

Posted

技术标签:

【中文标题】视图控制器 TDD【英文标题】:View Controller TDD 【发布时间】:2015-02-26 01:37:30 【问题描述】:

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

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

我要添加的两个测试是: 1) 检查 FirstViewController 中的按钮标题是否为“下一屏”。 2) 检查 SecondViewController 中的 textfield 是否为空,""。

我听说将您的 swift 文件添加到主要目标和测试目标的报告不是一个好习惯。但最好将您想要在测试中访问的任何内容公开并将主要目标导入测试。这就是我所做的。 (我还将主要目标的“定义模块”设置为“是”,因为这也是我在几篇文章中读到的)。

在 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 和视图控制器有什么经验?

我在 XCode 6.1.1 中使用 Swift。

提前致谢。

已解决

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

1) 我公开了任何我想测试的类/方法/变量。我不需要将 swift 文件添加到测试目标。

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

3) 实例化storyboard时,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(上面的链接)。

更新 #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)

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

【问题讨论】:

很好地更新了您的发现。这对其他人有帮助。 【参考方案1】:

我同意@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

查看我在布鲁克林斯威夫特的测试UIViewController 的演讲以了解详细信息:https://vimeo.com/115671189#t=37m50s(我的演讲开始于 37'50" 标记)。

【讨论】:

感谢您提供视频链接!这很有帮助。我已经用我的解决方案更新了 OP。 为了让你的代码更优雅,你也可以像这样断言视图:XCTAssertNotNil(secondViewController.view)。它还将触发生命周期流程。【参考方案2】:

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

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

我的建议是用这个替换那行:

让 dummy = viewController.view

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

有些人认为这种方法很老套,但它简单有效。 (见Clean way to force view to load subviews early)

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

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

【讨论】:

另外,我将代码文件添加到主目标和测试目标。你读到了什么表明这是一个坏主意? 我尝试了你的建议,用 let dummy = viewController.view 替换 viewController.viewDidLoad()。这导致错误提示“在 NSBundle 中找不到名为“Main”的故事板”或类似的内容。所以我尝试将故事板添加到测试目标并运行测试。结果是动态类转换错误。不太确定从这里去哪里。我读到这里向测试目标添加文件不是一个好主意twitter.com/modocache/status/549042409838219264 假设您有一个名为Car 的类,在Car.swift 中定义。当您将Car.swift 添加到您的应用程序和测试目标时,您最终会得到两种 类型:AppTarget.CarTestTarget.Car。这可能会导致几个问题: 1. 在您的应用程序代码中键入 isKindOfClass 之类的检查可能会失败——他们正在检查 AppTarget.Car,但您提供的是 TestTarget.Car。 2. 只需要执行一次的代码,比如+[Car load],将在AppTarget.Car 执行一次,对TestTarget.Car 执行一次。

以上是关于视图控制器 TDD的主要内容,如果未能解决你的问题,请参考以下文章

我应该测试我的控制器(MVC)吗?

TDD - 先写测试?

BDD与TDD开发模式的区别

text sqlite不支持ondelete('cascade'),faield tdd因为通过视图作曲家解决方案共享数据

控制器的 Laravel 单元测试

如何为我的 Spring Boot 控制器执行单元测试?