Search code examples
swiftnevpnmanager

How to stub the connection property (NEVPNConnection) in NEVPNManager?


I would like to extend the existing NetworkExtension classes by a protocol, in order to unit test my code.

I have first created the protocol for NEVPNManager

protocol NEVPNManagerProtocol {
    var connection : ConnectionProtocol { get } // <-- Doesn't work
    func loadFromPreferences(completionHandler: @escaping (Error?) -> Swift.Void)
    func saveToPreferences(completionHandler: ((Error?) -> Swift.Void)?)
}

extension NEVPNManager: NEVPNManagerProtocol {}

And then the separate protocol for connection property to stub it out.

protocol ConnectionProtocol {
    var status: NEVPNStatus { get }
    func stopVPNTunnel()
    func startVPNTunnel() throws
}

extension NEVPNConnection : ConnectionProtocol {}

Inside NEVPNManager I can see that I'm confirming to the property signature, and yet Xcode doesn't believe me and claims that:

Type 'NEVPNManager' does not conform to protocol 'NEVPNManagerProtocol'

And it tries to autocorrect it like this:

extension NEVPNManager: NEVPNManagerProtocol {
    var connection: ConnectionProtocol {
        <#code#>
    }
}

But checking the signature in NEVPNManager, it seems correct to me:

     /*!
     * @property connection
     * @discussion The NEVPNConnection object used for controlling the VPN tunnel.
     */
    @available(iOS 8.0, *)
    open var connection: NEVPNConnection { get }

Any advice?


Solution

  • Mocking this is tricky because Apple controls the instantiation of the NEVPNManager and its NEVPNConnection.

    The error you're seeing is because you are trying to redefine the connection property, and you can't do that. NEVPNManager already has a connection property of type NEVPNConnection.

    We can mock the connection property using a combination of your first protocol (modified) and a couple mock classes.

    First, the protocol needed to be tweaked slightly:

    protocol NEVPNManagerProtocol {
        var connection : NEVPNConnection { get } // <-- has to be this type
        func loadFromPreferences(completionHandler: @escaping (Error?) -> Swift.Void)
        func saveToPreferences(completionHandler: ((Error?) -> Swift.Void)?)
    }
    
    extension NEVPNManager: NEVPNManagerProtocol {}
    

    Next, we need a mock connection class, since the connection property must be a class of type NEVPNConnection, or one inheriting from that type. There doesn't seem to be much benefit to introducing a protocol here, since we are trying to mock the behavior of the class, which we can do more directly with a mock.

    class MockNEVPNConnection: NEVPNConnection {
        override var status: NEVPNStatus {
            return NEVPNStatus.connected //or whatever
        }
        override func stopVPNTunnel() {
            print("MockNEVPNConnection.stopVPNTunnel")
        }
        override func startVPNTunnel() throws {
            print("MockNEVPNConnection.startVPNTunnel")
        }
     }
    

    Finally, we need a mock manager class that returns a mock connection. Using a mock manager was the only way I was able to inject a mock connection.

    The mock manager conforms to the NEVPNManagerProtocol and returns our mock connection object. (Note: When trying to inherit directly from NEVPNManager, my playground crashed on instantiating the mock.)

    class MockNEVPNManager: NEVPNManagerProtocol {
        var connection: NEVPNConnection {
            return MockNEVPNConnection()
        }
        func loadFromPreferences(completionHandler: @escaping (Error?) -> Swift.Void) {
            print("MockNEVPNManager.loadFromPreferences")
        }
        func saveToPreferences(completionHandler: ((Error?) -> Swift.Void)?) {
            print("MockNEVPNManager.saveToPreferences")
        }
    }
    

    The client class must take an object of type NEVPNManagerProtocol and not NEVPNManager, so that we can pass the mock to it.

    class MyClient {
        let manager: NEVPNManagerProtocol
        init(manager: NEVPNManagerProtocol) {
            self.manager = manager
        }
    }
    

    In real life, we can pass the real manager to our client:

    let myClient = MyClient(manager: NEVPNManager.shared())
    

    In our test, we can pass the mock:

    let myMockedClient = MyClient(manager: MockNEVPNManager())
    

    And call methods on the connection:

    try? myMockedClient.manager.connection.startVPNTunnel()
    //prints "MockNEVPNConnection.startVPNTunnel"