CoreData之從項(xiàng)目重構(gòu)到Unit Test

寫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)代碼文件扬霜。

創(chuàng)建工程的時(shí)候勾選Include Unit Tests選項(xiàng)

由于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笛厦。如下圖所示:

在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è)試成功通過。

測(cè)試結(jié)果

至此幸逆,我們已經(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ì)你有所幫助_

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锦积,一起剝皮案震驚了整個(gè)濱河市芒帕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丰介,老刑警劉巖背蟆,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異哮幢,居然都是意外死亡带膀,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門家浇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來本砰,“玉大人,你說我怎么就攤上這事钢悲〉愣睿” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵莺琳,是天一觀的道長(zhǎng)还棱。 經(jīng)常有香客問我,道長(zhǎng)惭等,這世上最難降的妖魔是什么珍手? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上琳要,老公的妹妹穿的比我還像新娘寡具。我一直安慰自己,他們只是感情好稚补,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布童叠。 她就那樣靜靜地躺著,像睡著了一般课幕。 火紅的嫁衣襯著肌膚如雪厦坛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天乍惊,我揣著相機(jī)與錄音杜秸,去河邊找鬼。 笑死润绎,一個(gè)胖子當(dāng)著我的面吹牛撬碟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播莉撇,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼小作,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稼钩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤达罗,失蹤者是張志新(化名)和其女友劉穎坝撑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粮揉,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡巡李,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扶认。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侨拦。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辐宾,靈堂內(nèi)的尸體忽然破棺而出狱从,到底是詐尸還是另有隱情,我是刑警寧澤叠纹,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布季研,位于F島的核電站,受9級(jí)特大地震影響誉察,放射性物質(zhì)發(fā)生泄漏与涡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驼卖。 院中可真熱鬧氨肌,春花似錦、人聲如沸酌畜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)檩奠。三九已至桩了,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間埠戳,已是汗流浹背井誉。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留整胃,地道東北人颗圣。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓吨娜,卻偏偏與公主長(zhǎng)得像统锤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赢笨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)蛮寂、插件蔽午、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,033評(píng)論 4 62
  • 為什么要離婚呢?因?yàn)槟憬Y(jié)婚了啊酬蹋。 從初相識(shí)的互相感覺不錯(cuò)及老,到慢慢了解之后的堅(jiān)定,到最后進(jìn)入婚姻的殿堂范抓,都是在自己思...
    此木無言閱讀 595評(píng)論 0 0