CoreData之从项目重构到Unit Test

作者: MapleMeowMeow | 来源:发表于2016-07-04 21:18 被阅读286次

    写iOS与Swift相关代码也有一段时间了,UIKit与Foundation的一些组件用得也算比较溜了,但一直没有写过XCTest测试代码。今天以几天前完成的小应用TapList为例(该应用相关文章请点击这里),来简要介绍下iOS相关的Unit Test。


    简介

    Unit Test,即单元测试。一个良好的Unit Test应该具有以下这些特点:
    1.操作简便快速。
    2.各个Unit Test之间在功能上相互独立。
    3.可重复测试。
    4.有自我检测功能,即测试者不需要去额外看相关log即能知晓是否有Bug。
    5.经常为新代码准备相关Unit Test。。。

    在创建Xcode工程的时候,记得勾选Include Unit Tests选项(如下图所示),这样Xcode会在工程目录下自动帮你创建XXXTests文件夹,之后我们在这个文件夹下创建测试相关代码文件。

    创建工程的时候勾选Include Unit Tests选项

    由于Swift中class的访问权限默认是internal access level,即class只能在同一个module中互相访问。而要运行的app和相关test在两个不同的module中,所以要在test中访问并测试app的代码,只有以下3种途径:
    1.将app中相关class和其中的method标为public。
    2.将要测试的代码拷贝到test中。
    3.在app的相关代码前加上@testable标记。

    在TapList中,我们只对public api进行测试,故采用第一种方式。


    CoreData的测试小技巧

    由于基于SQLite的CoreData在disk上存储数据,故在测试中添加数据后需要手动将之删除,才能进行下次测试,这就违背了一个良好的Unit Test应该具有的操作简便快速原则和可重复测试原则。因此,在测试中,我们希望数据能够仅仅留在内存中,当一个测试结束的时候,内存中的数据就会消失,而不会影响下一次测试。

    因此,在测试中,我们不用SQLite作为CoreData的存储方式,而改用InMemory方式。


    原工程重构

    重构1

    我们希望InMemory方式的CoreData管理仅仅在原来存储模式的基础上改变数据库类型,其余则保持不变。因此,最好的办法就是构建一个子类继承原有CoreData的Stack,并对相关CoreData Stack组件进行重定义。Taplist的CoreData模型是在工程创建的时候由Xcode自动生成,首先,我们将之独立成一个类。

    打开CoreData-Taplist工程,创建CoreDataStack.swift文件,import CoreData,定义public class CoreDataStack,将Supporting Files下的AppDelegate.swift文件中CoreData相关代码,即4个属性定义和1个saveContext函数移到该类中。将managedObjectModel、persistentStoreCoordinator、managedObjectContext、func saveContext ()设为public,并添加public init()。代码如下:

    import Foundation
    import CoreData
    
    public class CoreDataStack {
    
        public init() {
            
        }
    
        lazy var applicationDocumentsDirectory: NSURL = {
            ...
        }()
    
        public lazy var managedObjectModel: NSManagedObjectModel = {
            ...
        }()
        ...
    }
    

    在AppDelegate.swift中添加coreDataStack属性。修补Xcode报出的一处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文件中,修改相关代码如下:

    lazy var context: NSManagedObjectContext = {
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        return appDelegate.coreDataStack.managedObjectContext
    }()
    

    运行工程,没有报错。进行简单操作,发现功能依旧,说明重构成功。

    重构2

    由于原工程比较简单,只有一个添加Item的操作,即ViewController.swift中的@IBAction func addItem()。为了方便进行Unit Test,我们为Item专门创建一个类,来管理有关Item的各项操作。

    首先在Item.swift中,将class Item设为public。在Item+CoreDataProperties.swift中,将extension也设为public。

    public class Item: NSManagedObject {
        ...
    }
    
    public extension Item {
        ...
    }
    

    在CoreData-TapList文件夹下新建ItemService.swift文件,其内部代码补全如下:

    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函数如下:

    @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的所有操作封装在一个独立的class中,方便后续测试。其中,ItemService类在初始化时传入一个NSManagedObjectContext,用来进行CoreData相关操作,这为我们后续测试改变CoreData存储类型做好了准备。

    运行工程,没有报错。进行添加操作,发现功能依旧,说明重构成功。


    Unit Test

    构建基于InMemory的CoreData Stack

    基于以上重构,TapLiat工程终于可以进行愉快的Unit Test了!还记得我们将要使用InMemory来进行测试么,那就先构建基于InMemory的CoreData Stack吧。

    在CoreData-TapListTests文件夹下新建Swift File,名为TestCoreDataStack,确保在Targets选项下只勾选CoreData-TapListTests。如下图所示:

    在Targets选项下只勾选CoreData-TapListTests

    补全其代码如下:

    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
            }()
        }
    }
    

    这样,就构建了CoreDataStack的子类,它用InMemory来存储数据。

    构建ItemServiceTests

    在CoreData-TapListTests文件夹下新建Unit Test Case Class,名为ItemServiceTests,Xcode已经自动帮你选好这是一个XCTestCase的子类。接着确保在Targets选项下只勾选CoreData-TapListTests。创建后可以看到ItemServiceTests里预置了不少测试函数。

    在ItemServiceTests中import相关模块:

    import CoreData
    import CoreData_TapList
    

    定义新属性:

    var itemService: ItemService!
    var coreDataStack: CoreDataStack!
    

    这里仍用CoreDataStack类型而不是TestCoreDataStack,是因为app中用的一直是CoreDataStack,并不是我们为了测试而建立的TestCoreDataStack。

    override func setUp()里,可以完成测试前的配置工作。我们在这里将coreDataStack用子类TestCoreDataStack初始化,并用它来初始化itemService,这样addItem的时候使用的就是基于InMemory的CoreData了。代码补全如下:

    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()里,可以完成测试结束后的清理工作。我们在这里将InMemory的测试数据清空。代码补全如下:

    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函数是测试文件给出的测试样例,在这里将之删除。

    定义自己的Unit Test函数如下:

    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)
    }
    

    在这个测试函数中,我们先用itemService.addItem函数添加了一个item,然后对返回结果进行Assert判断。如果所有Assert都通过,说明添加函数的功能正确。

    接下来运行测试代码,在Xcode菜单栏中Product选项中点击Test,或者快捷键Command + U,即运行了测试代码。

    测试结果如下图所示,说明所有测试成功通过。

    测试结果

    至此,我们已经完成了第一个Unit Test。


    CoreData didSave Test

    上一个Unit Test测试了addItem函数返回的数据,但是没有测试数据是否真的保存到了CoreData的store中。接下来我们要测试context的save过程。save过程对于测试者来说是透明的,所幸,我们可以通过NSManagedObjectContextDidSaveNotification来对save过程进行观察。

    在ItemServiceTests.swift中添加测试代码如下:

    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表示测试代码期待某个事件发生,这里我们用它来期待NSManagedObjectContextDidSaveNotification这个通知的产生。waitForExpectationsWithTimeout表示等待所期待的事件,括号中2.0表示等待2秒。如果在等待时间内,所期待的事件没有发生,则会产生error。因此在这里,通过assert产生的error是否为nil,就能判断save过程是否发生。

    运行测试代码,结果表明测试通过,说明CoreData确实保存了item数据。

    如果将以下代码从这个测试函数中删除,再次运行测试代码,则会产生Test Failed的提示信息,错误信息在该测试函数中显示。

    itemService.addItem("item1", score: 1)
    

    结语

    最终Demo已经上传到这里,希望这篇文章对你有所帮助_

    相关文章

      网友评论

        本文标题:CoreData之从项目重构到Unit Test

        本文链接:https://www.haomeiwen.com/subject/ymwcjttx.html