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?
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"