Swift Core Data Unit testing. Learn about types and modules while writing a non crashable core data unit test

Swift Core Data Unit testing. Learn about types and modules while writing a non crashable core data unit test

Let's use swift in production apps. This posts will be updated as we develop our production app in swift."Namespaces and Swift. They can result in crashes you would not expect."This post is about namespaces and Swift and how they can result in crashes you would not expect if you are used to Objective-C.

This post was written with Xcode 6.3. Xcode 7 has many improvements and introduces the @testable. Although this exists it could be still usefull to know why @testable is needed. This post tries to help you understand why it is needed.When you type a class in Swift you define it like

class Foo {

}

Instantiating would be:

let foo = Foo()

You do all this in an App target. App targets can be found in your Xcode project file. Typically if you create a new project named FooTargetName you have 2 targets:

- FooTargetName
- FooTargetNameTests

If we run the app in the simulator, the compiler will make this cool code look like this

let foo = FooTargetName.Foo()

If we run our test and the file Foo is added to our test target. (You can do this in your project file under compile sources) Then the code is compiled into:

let foo = FooTargetNameTests.Foo()

So if you would like to test class TestAbleClass then you add it to your compile sources of the test target and run the test.

Class compiled in FooTargetName and FooTargetNameTests

class TestAbleClass {
  let foo = Foo()
}

FooTargetNameTests

class TestAbleClassTests: XCTestCase {
  func test(){
     let testAbleClass = TestAbleClass()
     XCTAsssertNotNil(testAbleClass.foo)
}

This should all work except when you want to do some casting, things can get ugly. Lets define TestAbleClass as we would in Objective-C thinking.

class TestAbleClass {
  let foo: AnyObject

  init (){
    foo = Foo()
  }
  func getFoo() -> Foo {
    return foo as! FooTargetName.Foo
  }
}

The same code will now crash on foo as! FooTargetName.Foo

This may seam like a trivial issue because why would you write foo as! FooTargetName.Foo instead of foo as! Foo. When part of your code runs in Objective-C, like Core Data does, you have no choice.

This post will solve the namespace problem when using Core Data. We tackle the problem by avoiding the Objective-C space. When writing code like the snippet below you get sucked into the Objective-C space.

@Objective-C(classname)
@objc

This post is about avoiding the need for the stuff above when using core data.The first hurdle we faced when writing unit tests in swift was how to use our app code in the swift target. Swift has a namespace that can bite you when you write unit tests with mixed Objective-C code. And because Core Data is written in Objective-C, you cannot avoid this.

Thanks to Bart Kockx for explaining this to me!

We will first define some rules and then give a coding examples.

  • * Do not add your Swift files to both app and unit targets. Adding it to the app target will do.
  • * Import the app module *import \** Until Swift 2.0 comes out, we have to mark all method/classes/protocols as public if we want to test then

Keeping this in mind, let's setup the Core Data stack for unit testing.

import Foundation
import XCTest

extension XCTestCase {
    /*
    * Call these methods in setup and tear down of methods accessing core data
    */
    func setUpCoreData () {
        // Cleanup Core Data setup because there already is a context setup in the
        // AppDelegate. We do not want to use this context and therefore we create
        // an in memory one.
        MagicalRecord.cleanUp()
        MagicalRecord.setupCoreDataStackWithInMemoryStore()
    }

    func tearDownCoreData () {
        MagicalRecord.cleanUp()
    }
}

class CoreDataTestCase : XCTestCase {

    override func setUp() {
        super.setUp()
        setUpCoreData()
    }

    override func tearDown() {
        super.tearDown()
        tearDownCoreData()
    }

If you extend your unit test class from the class above, then you'll be fine.

You have to make sure that your objects created by Core Data are type safe, as Swift is type safe and Objective-C is not. So this can become very tricky. We made a core data controller using generics that tries to overcome this hurdle. It is not optimal but it works. We will first share that code and then write a test using that code.

import Foundation

public class CoreDataController  {

    let entityName = NSStringFromClass(ResultType.self).componentsSeparatedByString(".").last!

    public init (){
        //Empty by design
    }

	public func createEntity (context: NSManagedObjectContext = NSManagedObjectContext.MR_defaultContext()) -> ResultType {
		let description = entityDescription(context: context)
		let entity = ResultType(entity: description, insertIntoManagedObjectContext: context)

		return entity
    }

    public func entityDescription (context: NSManagedObjectContext = NSManagedObjectContext.MR_defaultContext()) -> NSEntityDescription{
        let description = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)

        return description!
    }
}
import Foundation
import CoreData

public class Foo: NSManagedObject {

    @NSManaged public var name: NSDate

    //MARK:- Called from import methods of Magical record

    static func entityInManagedObjectContext(context: NSManagedObjectContext) -> NSEntityDescription {
        return CoreDataController().entityDescription(context: context)
    }

    static func insertInManagedObjectContext(context: NSManagedObjectContext) -> NSManagedObject{

        return CoreDataController().createEntity(context: context)
    }
}
import XCTest
import AppTargetName

class FooTargetNameTests: CoreDataTestCase {
    func testCreate() {
        let foo = CoreDataController().createEntity()
        if let foo = foo as? Foo {
        }else {
          XCTFail("We could not cast in unit tests.")
        }
    }
}

If you do not make your tests as described above the class of Foo in your app target will be AppTargetName.Foo and in tests AppTargetNameTests.Foo so every time you try to cast using 'as!', you're unit test will crash.

Another important line of code is let entityName = NSStringFromClass(ResultType.self).componentsSeparatedByString(".").last! this strips the AppTargetName. from your class as CoreData works without that name.

So thats it maybe more next week/tomorrow or whenever we find usefull stuff when writing swift production apps.