ViewController是iOS应用开发的支柱之一, 它们是用户界面和业务逻辑、模型的纽带 ,即便只是修改了它其中的一点代码也可能会引发严重的问题,因此对ViewController进行测试就变得极为重要。然而,为ViewController写测试代码一直以来都是一个晦涩难懂的话题。这篇文章中,我会尝试使用 Quick和Nimble 框架来解开这些疑惑。
Quick是一个针对于Swift和Objective-C的 行为驱动开发 框架. 它的灵感来自于
RSpec , Specta , 和 Ginkgo .
Nimble是一个同时适用于Swift和Objective-C语言的匹配框架.
换句话说,Quick是一个用于创建 BDD 测试的框架。配合Nimbl,可以为你创建更符合预期目标的测试。
这个示例是使用Swift 1.2创建的名为Pony的iOS应用。这个应用含有一个底部栏和一个用于在应用启动时弹出应用简介信息的ViewController。Main.storybard看起来如下图所示 :
运行后 :
( 如果浏览器不支持视频播放,可以手动拷贝地址到浏览器中观看,地址在 https://d262ilb51hltx0.cloudfront.net/max/1600/1*9HuloDCQQ3Ul2t4KGQGkYg.ogv ).
如果你安装了最新版的 CocoaPods ,你可以将下面的配置代码添加到Podfile中 :
platform :ios, '8.0' source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'PonyTests', :exclusive => true do pod 'Nimble', :git => 'https://github.com/Quick/Nimble.git' #, :branch => 'swift-1.1' # if you want to use swift 1.1 pod 'Quick', :git => 'https://github.com/Quick/Quick.git', # :branch => 'swift-1.1' end
通过
Rakefile手动创建
public class MyPublicClass { public var myPublicProperty: String? public func myPublicFunc () { //... } }
如果Quick模板已经安装了,那么你可以选择quick模板、创建一个新的文件,然后import应用模块到文件中,例如 :
import Quick import Nimble import MyAppModule // Importing the app module class HelloTest: QuickSpec { override func spec() { //... } }
如果你导入应用模块有问题,可以参考这篇文章:
http://stackoverflow.com/a/24151067/1970675PonyTabController是UITabController的子类,它的职责是在第一次进入应用时展示应用简介(appIntroViewController)。
public class PonyTabController: UITabBarController { override public func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let userDefaults = NSUserDefaults.standardUserDefaults() if !userDefaults.boolForKey("appIntroHasBeenPresented") { let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()) let appIntroViewController = storyboard.instantiateViewControllerWithIdentifier("appIntroViewControllerID") as! AppIntroViewController appIntroViewController.delegate = self self.presentViewController(appIntroViewController, animated: true) { userDefaults.setBool(true, forKey: "appIntroHasBeenPresented") } } } } extension PonyTabController: AppIntroDelegate { // MARK: - AppIntroDelegate public func appIntroDidFinish(appIntro: UIViewController!) { // Dismissing app intro dismissViewControllerAnimated(true, completion:nil) } }
让我们从viewDidAppear中开始 :
“当应用简介从来没有被dismissed时,它会被当做appIntroDelegate设置给PonyTabController”
import Quick import Nimble import Pony class PonyTabBarControllerSpec: QuickSpec { override func spec() { var tabBarController: PonyTabController! describe("viewDidAppear"){ describe("When app intro had never been dismissed"){ it("should be set as the appIntroDelegate"){ } } } } }
现在我们有了期望的描述信息,但是我们还缺少一些东西。我们还没有断言或者用于测试的对象,也没有任何的测试方法会被调用。为了更简介的进行初始化,我们可以将测试代码分为三个部分,例如 :
完成的测试代码 :
import Quick import Nimble import Pony class PonyTabBarControllerSpec: QuickSpec { override func spec() { var tabBarController: PonyTabController! describe(".viewDidAppear"){ describe("When app intro had never been dismissed"){ var appIntroViewController: AppIntroViewController? beforeEach{ // Arrange: NSUserDefaults.standardUserDefaults().setBool(false, forKey: "appIntroHasBeenPresented") let storyboard = UIStoryboard(name:"Main", bundle: NSBundle.mainBundle()) tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController let window = UIWindow(frame: UIScreen.mainScreen().bounds) window.makeKeyAndVisible() window.rootViewController = tabBarController // Act: tabBarController.beginAppearanceTransition(true, animated: false) // Triggers viewWillAppear tabBarController.endAppearanceTransition() // Triggers viewDidAppear appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController! } it("should be set as the appIntroDelegate"){ // Assert: expect(appIntroViewController!.delegate as? PonyTabController).to(equal(tabBarController)) } } } } }
通过 Arrange-Act-Assert 模式组织测试代码是一种非常好的实践,它的优点如下 :
现在,做同样的操作 :
“** 当应用简介从来没有被dismissed时,它应该被展示****”
import Quick import Nimble import Pony class PonyTabBarControllerSpec: QuickSpec { override func spec() { var tabBarController: PonyTabController! describe(".viewDidAppear"){ context("When app intro had never been dismissed"){ var appIntroViewController: AppIntroViewController? beforeEach{ // Arrange: NSUserDefaults.standardUserDefaults().setBool(false, forKey: "appIntroHasBeenPresented") let storyboard = UIStoryboard(name:"Main", bundle: NSBundle.mainBundle()) tabBarController = storyboard.instantiateInitialViewController() as! PonyTabController let window = UIWindow(frame: UIScreen.mainScreen().bounds) window.makeKeyAndVisible() window.rootViewController = tabBarController // Act: tabBarController.beginAppearanceTransition(true, animated: false) // Triggers viewWillAppear tabBarController.endAppearanceTransition() // Triggers viewDidAppear appIntroViewController = tabBarController.presentedViewController as! AppIntroViewController! } it("should be set as the appIntroDelegate"){ // Assert: expect(appIntroViewController!.delegate as? PonyTabController).to(equal(tabBarController)) } it("should be presented"){ // Assert: expect(tabBarController.presentedViewController).toEventually(beAnInstanceOf(AppIntroViewController)) } } } } }
import Quick import Nimble import Pony class PonyTabBarControllerSpec: QuickSpec { override func spec() { var tabBarController: PonyTabController! describe(".viewDidAppear"){ describe("When app intro had not been presented"){ // ... previous test context("and appIntroDidFinish is called") { // Introducing a context let userDefaults = NSUserDefaults.standardUserDefaults() beforeEach { // Arrange: userDefaults.setBool(false, forKey: "appIntroHasBeenPresented") // Act: // Triggers viewWillAppear and viewDidAppear:animated tabBarController.beginAppearanceTransition(true, animated: false) tabBarController.endAppearanceTransition() // - Dismissing app intro. tabBarController.appIntroDidFinish(appIntroViewController) } it("should dismiss app intro"){ // Assert: expect(appIntroViewController!.isBeingDismissed()).toEventually(beTrue()) } } } } } }
“describe” 和 “context”的简单介绍:
Describe: 包装一个功能测试集合;
Context:在同一状态下包装一个功能测试集合;
Nimble含有一个 waitUntil 方法,你能够在它里面执行闭包代码,当done函数被调用时表明它已经ready,如果done函数没有被调用,那么第二个测试将会失败。如果你需要扩展定时函数来指定一个超时参数来确定你在执行某个函数时多长时间视为失败。例如,当你想等待一个ViewController被展示时你可以这么处理 :
waitUntil{ done in tabBarController.presentViewController(viewController, animated: false){ done() } }