Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock third party classes (Firebase) in Swift

I'm trying to unit test a class of my own which is calling a method on a third party class:

FIRAuth.auth()?.signInAnonymously() { (user, error) in
    //
}

I'm using protocol based dependency injection to achieve this:

protocol FIRAuthProtocol {
    func signInAnonymously(completion: FIRAuthResultCallback?)
}
extension FIRAuth: FIRAuthProtocol {}

class MyClass {
    private var firAuth: FIRAuthProtocol

    init(firAuth: FIRAuthProtocol) {
        self.firAuth = firAuth
    }

    func signIn() {
        firAuth.signInAnonymously() { (user, error) in
            //
        }
    }
}

class MockFIRAuth: FIRAuthProtocol {
    var signInAnonymouslyCalled = false

    func signInAnonymously(completion: FIRAuthResultCallback? = nil) {
        signInAnonymouslyCalled = true
    }

}

class MyClassSpec: QuickSpec {
    override func spec() {
        describe("MyClass") {
            describe(".signIn()") {
                it("should call signInAnonymously() on firAuth") {
                    let mockFIRAuth = MockFIRAuth()
                    let myClass = MyClass(firAuth: mockFIRAuth)
                    expect(mockFIRAuth.signInAnonymouslyCalled).to(beFalse())
                    myClass.signIn()
                    expect(mockFIRAuth.signInAnonymouslyCalled).to(beTrue())
                }
            }
        }
    }
}

So far so good! Now, I'd like my mockFIRAuth to return an instance of FIRUser. Here's my issue: I can't create an instance of FIRUser myself.

FYI: public typealias FIRAuthResultCallback = (FIRUser?, Error?) -> Swift.Void

If found this great article which explains how to make a method on a third party class return a protocol instead of a type. http://masilotti.com/testing-nsurlsession-input/ Maybe my situation is different than the article's, but here's my shot at this:

I've defined a FIRUserProtocol:

protocol FIRUserProtocol {
    var uid: String { get }
}
extension FIRUser: FIRUserProtocol {}

I've updated my FIRAuthProtocol to call the completion handler with FIRUserProtocol instead of FIRUser:

protocol FIRAuthProtocol {
    func signInAnonymously(completion: ((FIRUserProtocol?, Error?) -> Void)?)
}

I've updated my FIRAuth extension to support the modified protocol. My newly defined method calls the default implementation of signInAnonymously:

extension FIRAuth: FIRAuthProtocol {
    func signInAnonymously(completion: ((FIRUserProtocol?, Error?) -> Void)? = nil) {
        signInAnonymously(completion: completion)
    }
}

Finally, I've updated MockFIRAuth to support the modified protocol:

class MockFIRAuth: FIRAuthProtocol {
    var signInAnonymouslyCalled = false
    func signInAnonymously(completion: ((FIRUserProtocol?, Error?) -> Void)? = nil) {
        signInAnonymouslyCalled = true
    }  
}

Now, when I run my test everything comes to a crashing halt:

Thread 1: EXC_BAD_ACCESS (code=2, address=0x7fff586a2ff8) Thread 1: EXC_BAD_ACCESS (code=2, address=0x7fff586a2ff8)

Please advice!

Update

After renaming the completion argument label in my FIRAuthProtocol's method everything seems to work as expected:

protocol FIRAuthProtocol {
    func signInAnonymously(completionWithProtocol: ((FIRUserProtocol?, Error?) -> Void)?)
}

extension FIRAuth: FIRAuthProtocol {
    func signInAnonymously(completionWithProtocol: ((FIRUserProtocol?, Error?) -> Void)? = nil) {
        signInAnonymously(completion: completionWithProtocol)
    }
}

This solves my issue for now, but I'd still like to know why my first attempt was unsuccessful. Does this mean that the two methods with different parameter types in their closures can't be told apart, which was causing my app to crash?

like image 706
Pim Avatar asked Nov 12 '16 09:11

Pim


1 Answers

I've finally found an elegant way to solve this.

protocol FIRAuthProtocol {
    func signInAnonymously(completion: ((FIRUserProtocol?, Error?) -> Void)?)
}

extension FIRAuth: FIRAuthProtocol {
    func signInAnonymously(completion: ((FIRUserProtocol?, Error?) -> Void)? = nil) {
        let completion = completion as FIRAuthResultCallback?
        signInAnonymously(completion: completion)
    }
}

This way, there's no need to alter function names or argument labels.

like image 147
Pim Avatar answered Sep 29 '22 17:09

Pim