寫iOS與Swift相關(guān)代碼也有一段時(shí)間了册养,UIKit與Foundation的一些組件用得也算比較溜了茎用,但一直沒有寫過XCTest測(cè)試代碼昨登。今天以幾天前完成的小應(yīng)用TapList為例(該應(yīng)用相關(guān)文章請(qǐng)點(diǎn)擊這里)棘捣,來簡(jiǎn)要介紹下iOS相關(guān)的Unit Test门坷。
簡(jiǎn)介
Unit Test讶坯,即單元測(cè)試番电。一個(gè)良好的Unit Test應(yīng)該具有以下這些特點(diǎn):
1.操作簡(jiǎn)便快速。
2.各個(gè)Unit Test之間在功能上相互獨(dú)立辆琅。
3.可重復(fù)測(cè)試漱办。
4.有自我檢測(cè)功能,即測(cè)試者不需要去額外看相關(guān)log即能知曉是否有Bug婉烟。
5.經(jīng)常為新代碼準(zhǔn)備相關(guān)Unit Test娩井。。似袁。
在創(chuàng)建Xcode工程的時(shí)候洞辣,記得勾選Include Unit Tests選項(xiàng)(如下圖所示),這樣Xcode會(huì)在工程目錄下自動(dòng)幫你創(chuàng)建XXXTests文件夾昙衅,之后我們?cè)谶@個(gè)文件夾下創(chuàng)建測(cè)試相關(guān)代碼文件扬霜。
由于Swift中class的訪問權(quán)限默認(rèn)是internal access level,即class只能在同一個(gè)module中互相訪問而涉。而要運(yùn)行的app和相關(guān)test在兩個(gè)不同的module中著瓶,所以要在test中訪問并測(cè)試app的代碼,只有以下3種途徑:
1.將app中相關(guān)class和其中的method標(biāo)為public啼县。
2.將要測(cè)試的代碼拷貝到test中材原。
3.在app的相關(guān)代碼前加上@testable標(biāo)記沸久。
在TapList中,我們只對(duì)public api進(jìn)行測(cè)試华糖,故采用第一種方式麦向。
CoreData的測(cè)試小技巧
由于基于SQLite的CoreData在disk上存儲(chǔ)數(shù)據(jù),故在測(cè)試中添加數(shù)據(jù)后需要手動(dòng)將之刪除客叉,才能進(jìn)行下次測(cè)試,這就違背了一個(gè)良好的Unit Test應(yīng)該具有的操作簡(jiǎn)便快速原則和可重復(fù)測(cè)試原則话告。因此兼搏,在測(cè)試中,我們希望數(shù)據(jù)能夠僅僅留在內(nèi)存中沙郭,當(dāng)一個(gè)測(cè)試結(jié)束的時(shí)候佛呻,內(nèi)存中的數(shù)據(jù)就會(huì)消失,而不會(huì)影響下一次測(cè)試病线。
因此吓著,在測(cè)試中,我們不用SQLite作為CoreData的存儲(chǔ)方式送挑,而改用InMemory方式绑莺。
原工程重構(gòu)
重構(gòu)1
我們希望InMemory方式的CoreData管理僅僅在原來存儲(chǔ)模式的基礎(chǔ)上改變數(shù)據(jù)庫(kù)類型,其余則保持不變惕耕。因此纺裁,最好的辦法就是構(gòu)建一個(gè)子類繼承原有CoreData的Stack,并對(duì)相關(guān)CoreData Stack組件進(jìn)行重定義司澎。Taplist的CoreData模型是在工程創(chuàng)建的時(shí)候由Xcode自動(dòng)生成欺缘,首先,我們將之獨(dú)立成一個(gè)類挤安。
打開CoreData-Taplist工程谚殊,創(chuàng)建CoreDataStack.swift文件,import CoreData蛤铜,定義public class CoreDataStack嫩絮,將Supporting Files下的AppDelegate.swift文件中CoreData相關(guān)代碼,即4個(gè)屬性定義和1個(gè)saveContext函數(shù)移到該類中昂羡。將managedObjectModel絮记、persistentStoreCoordinator、managedObjectContext虐先、func saveContext ()設(shè)為public怨愤,并添加public init()。代碼如下:
import Foundation
import CoreData
public class CoreDataStack {
public init() {
}
lazy var applicationDocumentsDirectory: NSURL = {
...
}()
public lazy var managedObjectModel: NSManagedObjectModel = {
...
}()
...
}
在AppDelegate.swift中添加coreDataStack屬性蛹批。修補(bǔ)Xcode報(bào)出的一處Bug撰洗。代碼如下:
func applicationWillTerminate(application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
coreDataStack.saveContext()
}
// MARK: - Core Data stack
let coreDataStack = CoreDataStack()
在唯一用到原managedObjectContext的ViewController.swift文件中篮愉,修改相關(guān)代碼如下:
lazy var context: NSManagedObjectContext = {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
return appDelegate.coreDataStack.managedObjectContext
}()
運(yùn)行工程,沒有報(bào)錯(cuò)差导。進(jìn)行簡(jiǎn)單操作试躏,發(fā)現(xiàn)功能依舊,說明重構(gòu)成功设褐。
重構(gòu)2
由于原工程比較簡(jiǎn)單颠蕴,只有一個(gè)添加Item的操作,即ViewController.swift中的@IBAction func addItem()助析。為了方便進(jìn)行Unit Test犀被,我們?yōu)镮tem專門創(chuàng)建一個(gè)類,來管理有關(guān)Item的各項(xiàng)操作外冀。
首先在Item.swift中寡键,將class Item設(shè)為public。在Item+CoreDataProperties.swift中雪隧,將extension也設(shè)為public西轩。
public class Item: NSManagedObject {
...
}
public extension Item {
...
}
在CoreData-TapList文件夾下新建ItemService.swift文件,其內(nèi)部代碼補(bǔ)全如下:
import Foundation
import CoreData
import UIKit
public class ItemService {
let managedObjectContext: NSManagedObjectContext
public init(managedObjectContext: NSManagedObjectContext) {
self.managedObjectContext = managedObjectContext
}
public func addItem(name: String, score: NSNumber) -> Item {
let item = NSEntityDescription.insertNewObjectForEntityForName("Item", inManagedObjectContext: self.managedObjectContext) as! Item
item.name = name
item.score = score
item.image = UIImage(named: "meow")
do {
try self.managedObjectContext.save()
} catch let error as NSError {
print("Error: \(error.userInfo)")
}
return item
}
}
在ViewController.swift中脑沿,修改addItem函數(shù)如下:
@IBAction func addItem() {
let alert = UIAlertController(title: "Add Item", message: nil, preferredStyle: .Alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
let saveAction = UIAlertAction(title: "Save", style: .Default) { (action) in
let nameField = alert.textFields![0]
let scoreField = alert.textFields![1]
let itemService = ItemService(managedObjectContext: self.context)
itemService.addItem(nameField.text!, score: Int(scoreField.text!) ?? 0)
}
alert.addTextFieldWithConfigurationHandler { (textField) in
textField.placeholder = "name"
}
alert.addTextFieldWithConfigurationHandler { (textField) in
textField.placeholder = "score"
}
alert.addAction(cancelAction)
alert.addAction(saveAction)
self.presentViewController(alert, animated: true, completion: nil)
}
以上修改是為了將Item的所有操作封裝在一個(gè)獨(dú)立的class中藕畔,方便后續(xù)測(cè)試。其中捅伤,ItemService類在初始化時(shí)傳入一個(gè)NSManagedObjectContext劫流,用來進(jìn)行CoreData相關(guān)操作,這為我們后續(xù)測(cè)試改變CoreData存儲(chǔ)類型做好了準(zhǔn)備丛忆。
運(yùn)行工程祠汇,沒有報(bào)錯(cuò)。進(jìn)行添加操作熄诡,發(fā)現(xiàn)功能依舊可很,說明重構(gòu)成功。
Unit Test
構(gòu)建基于InMemory的CoreData Stack
基于以上重構(gòu)凰浮,TapLiat工程終于可以進(jìn)行愉快的Unit Test了我抠!還記得我們將要使用InMemory來進(jìn)行測(cè)試么,那就先構(gòu)建基于InMemory的CoreData Stack吧袜茧。
在CoreData-TapListTests文件夾下新建Swift File菜拓,名為TestCoreDataStack,確保在Targets選項(xiàng)下只勾選CoreData-TapListTests笛厦。如下圖所示:
補(bǔ)全其代碼如下:
import Foundation
import CoreData
import CoreData_TapList
class TestCoreDataStack: CoreDataStack {
override init() {
super.init()
self.persistentStoreCoordinator = {
let psc = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
do {
try psc.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil)
} catch let error as NSError {
print("ERROR: \(error.userInfo)")
}
return psc
}()
}
}
這樣纳鼎,就構(gòu)建了CoreDataStack的子類,它用InMemory來存儲(chǔ)數(shù)據(jù)。
構(gòu)建ItemServiceTests
在CoreData-TapListTests文件夾下新建Unit Test Case Class贱鄙,名為ItemServiceTests劝贸,Xcode已經(jīng)自動(dòng)幫你選好這是一個(gè)XCTestCase的子類。接著確保在Targets選項(xiàng)下只勾選CoreData-TapListTests逗宁。創(chuàng)建后可以看到ItemServiceTests里預(yù)置了不少測(cè)試函數(shù)映九。
在ItemServiceTests中import相關(guān)模塊:
import CoreData
import CoreData_TapList
定義新屬性:
var itemService: ItemService!
var coreDataStack: CoreDataStack!
這里仍用CoreDataStack類型而不是TestCoreDataStack,是因?yàn)閍pp中用的一直是CoreDataStack瞎颗,并不是我們?yōu)榱藴y(cè)試而建立的TestCoreDataStack件甥。
override func setUp()里,可以完成測(cè)試前的配置工作言缤。我們?cè)谶@里將coreDataStack用子類TestCoreDataStack初始化嚼蚀,并用它來初始化itemService,這樣addItem的時(shí)候使用的就是基于InMemory的CoreData了管挟。代碼補(bǔ)全如下:
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
coreDataStack = TestCoreDataStack()
camperService = CamperService(managedObjectContext: coreDataStack.mainContext, coreDataStack: coreDataStack)
}
override func tearDown()里,可以完成測(cè)試結(jié)束后的清理工作弄捕。我們?cè)谶@里將InMemory的測(cè)試數(shù)據(jù)清空僻孝。代碼補(bǔ)全如下:
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
itemService = nil
coreDataStack = nil
}
testExample和testPerformanceExample函數(shù)是測(cè)試文件給出的測(cè)試樣例,在這里將之刪除守谓。
定義自己的Unit Test函數(shù)如下:
func testAddItem() {
let item = itemService.addItem("item1", score: 30)
XCTAssertNotNil(item, "item should not be nil")
XCTAssertTrue(item.name == "item1")
XCTAssertTrue(item.score!.integerValue == 30)
}
在這個(gè)測(cè)試函數(shù)中穿铆,我們先用itemService.addItem函數(shù)添加了一個(gè)item,然后對(duì)返回結(jié)果進(jìn)行Assert判斷斋荞。如果所有Assert都通過荞雏,說明添加函數(shù)的功能正確。
接下來運(yùn)行測(cè)試代碼平酿,在Xcode菜單欄中Product選項(xiàng)中點(diǎn)擊Test凤优,或者快捷鍵Command + U,即運(yùn)行了測(cè)試代碼蜈彼。
測(cè)試結(jié)果如下圖所示筑辨,說明所有測(cè)試成功通過。
至此幸逆,我們已經(jīng)完成了第一個(gè)Unit Test棍辕。
CoreData didSave Test
上一個(gè)Unit Test測(cè)試了addItem函數(shù)返回的數(shù)據(jù),但是沒有測(cè)試數(shù)據(jù)是否真的保存到了CoreData的store中还绘。接下來我們要測(cè)試context的save過程楚昭。save過程對(duì)于測(cè)試者來說是透明的,所幸拍顷,我們可以通過NSManagedObjectContextDidSaveNotification來對(duì)save過程進(jìn)行觀察抚太。
在ItemServiceTests.swift中添加測(cè)試代碼如下:
func testContextIsSavedAfterAddingItem() {
expectationForNotification(NSManagedObjectContextDidSaveNotification, object: coreDataStack.managedObjectContext) { (notification) -> Bool in
return true
}
itemService.addItem("item1", score: 1)
waitForExpectationsWithTimeout(2.0) { (error) in
XCTAssertNil(error, "Save did not occur")
}
}
這里用到了XCTest的expectation,expectation表示測(cè)試代碼期待某個(gè)事件發(fā)生菇怀,這里我們用它來期待NSManagedObjectContextDidSaveNotification這個(gè)通知的產(chǎn)生凭舶。waitForExpectationsWithTimeout表示等待所期待的事件晌块,括號(hào)中2.0表示等待2秒。如果在等待時(shí)間內(nèi)帅霜,所期待的事件沒有發(fā)生匆背,則會(huì)產(chǎn)生error。因此在這里身冀,通過assert產(chǎn)生的error是否為nil钝尸,就能判斷save過程是否發(fā)生。
運(yùn)行測(cè)試代碼搂根,結(jié)果表明測(cè)試通過珍促,說明CoreData確實(shí)保存了item數(shù)據(jù)。
如果將以下代碼從這個(gè)測(cè)試函數(shù)中刪除剩愧,再次運(yùn)行測(cè)試代碼猪叙,則會(huì)產(chǎn)生Test Failed的提示信息,錯(cuò)誤信息在該測(cè)試函數(shù)中顯示仁卷。
itemService.addItem("item1", score: 1)
結(jié)語(yǔ)
最終Demo已經(jīng)上傳到這里穴翩,希望這篇文章對(duì)你有所幫助_。