Allow var to hold parent or sub-class type

Hi,


My motivation is mocking objects for unit testing, but the problem seems more that I'm not understanding the language (inheritance?) itself. So I condensed things down (hopefully) into a single Swift command line tool, to show the essence of what I'm stuck with. I can see what the problem is, I just don't know the right (Swift-like) way to approach it.


On line 80 I want to set a properly of the sub-class MockNetworkHandler, but I can't because the property is not visible (compiler error). I realise that mockClient.netHandler is of type NetworkHandler, which is (I believe) why the compiler says no.


Why does mockClient.netHandler take a sub-class type, yet not let me access that types properties?

What is the best approach to get functionality that lets me do simple logic like this, in a mocked class?


thanks,


//
//  main.swift
//  Demo
//
//  Created by Doug Bridgens on 16/04/2020.
//  Copyright © 2020 Doug Bridgens. All rights reserved.
//

import Foundation

// my principle class for network handling
public class NetworkHandler {
    
    public init() { }

    func websiteStatus(_ fullURL: String, _ completion: @escaping (Int) -> Void) {

        if let url = URL(string: fullURL) {
            var request = URLRequest(url: url)
            request.httpMethod = "HEAD"
            
            let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                
                if let httpResponse = response as? HTTPURLResponse {
                    completion(httpResponse.statusCode)
                }
            })
            task.resume()
        }
    }
}

// I mock this class to be able to shortcut the async dataTask() call, and
// to be able to control the network responses (for unit testing)
public class MockNetworkHandler: NetworkHandler {
    
    // this var should let me switch on/off the network for unit tests
    var websiteActive = true

    public override init() {
        super.init()
    }
    
    override func websiteStatus(_ fullURL: String, _ completion: @escaping (Int) -> Void) {
        if websiteActive {
            completion(801)  // 801 just to differentiate from a real request in this example
        }
        else{
            completion(901)  // 901 just to differentiate from a real request in this example
        }
    }
}

public class AppClient {
    
    var netHandler: NetworkHandler?
    
    public init() {
        netHandler = NetworkHandler()
    }
    public convenience init(mockNetHandler: MockNetworkHandler) {
        self.init()
        netHandler = mockNetHandler
    }
}


// normal app flow
let client = AppClient()
let statusCompletion: (Int) -> Void = { status in
    print("status \(status)")
}
// uncomment to make real requests
//client.netHandler?.websiteStatus("https://apple.com", statusCompletion)

// flow using mocked network handler, this is simulating what I want to do in unit tests
let mockClient = AppClient(mockNetHandler: MockNetworkHandler())

// PROBLEM: the following line is a compiler error: Value of type 'NetworkHandler?' has no member 'websiteActive'
//mockClient.netHandler.websiteActive = false

mockClient.netHandler?.websiteStatus("https://apple.com", statusCompletion)


dispatchMain()  // stay alive, allowing async calls to complete

Replies

Look:


line 38

var websiteActive = true

is a property of MockNetworkHandler, not of its parent: NetworkHandler


If you just move it to the parent, that should work:

move line 38 to line 13.

Yes, I don't want it in the parent as that would mean test code polluting the main code. With mocking I can have all the mocked classes in my test modules.


thanks

So, I had a thought. Setting my var via a convenience initialiser. And it seems to work fine.


Modified my mock class:

public class MockNetworkHandler: NetworkHandler {
    
    // this var should let me switch on/off the network for unit tests
    var websiteActive: Bool

    public override init() {
        self.websiteActive = true // default
        super.init()
    }
    public convenience init(networkActive: Bool = true) {
        self.init()
        websiteActive = networkActive
    }
    
    override func websiteStatus(_ fullURL: String, _ completion: @escaping (Int) -> Void) {
        if websiteActive {
            completion(801)  // 801 just to differentiate from a real request in this example
        }
        else{
            completion(901)  // 901 just to differentiate from a real request in this example
        }
    }
}


In use:

var mockClient = AppClient(mockNetHandler: MockNetworkHandler(networkActive: true))
mockClient.netHandler?.websiteStatus("https://apple.com", statusCompletion)

mockClient = AppClient(mockNetHandler: MockNetworkHandler(networkActive: false))
mockClient.netHandler?.websiteStatus("https://apple.com", statusCompletion)


Output:

status 801
status 901

I'm not sure I understand what the purpose of your test.


Yes, you can do as you describe.


But, why do you define netHandler as NetworkHandler and not MockNetworkHandler


public class AppClient {
  
    var netHandler: MockNetworkHandler?

I am trying to work out the best way to unit test classes that depend on networking. I do not want any unit test code/scaffolding in the main code-base. The main code is a Swift package, with unit tests alongside.


So, here is an example of two unit tests I have which test the outcome of device.processQueue(). I must test this for both the network being active and inactive.


    func testDeviceProcessQueueConnReady() {
        
        let device = CKDevice()
        device.conn = ConnectionMock(networkResponse: .ready)

        device.submitToQueue(payload: "some data")  // create some data to send
        device.startConnection()
        device.processQueue()
        
        let sendQueueLength = device.sendQueue.count
        
        XCTAssertEqual(0, sendQueueLength)  // payload should have been sent
    }
    
    func testDeviceProcessQueueConnNotReady() {
        
        let device = CKDevice()
        device.conn = ConnectionMock(networkResponse: .preparing)

        device.submitToQueue(payload: "some data")  // create some data to send
        device.startConnection()
        device.processQueue()
        
        let sendQueueLength = device.sendQueue.count
        
        XCTAssertEqual(1, sendQueueLength)  // payload should still be queued
    }


And I create the mock class in my unit test modules:


public class ConnectionMock: Connection {

    var networkState = NWConnection.State.ready

    init() {
        super.init(host: "127.0.0.1", port: "8000")
    }
    convenience init(networkResponse: NWConnection.State) {
        self.init()
        self.networkState = networkResponse
    }

    public override func startConnection() {
        self.delegate?.connectionDidUpdate(self, (NWConnection.State.ready == networkState))
    }
    
    public override func sendMessage(queueID: String, messageString: String) {
        // immediate response for unitests, rather than async in normal code flow
        self.delegate?.messageProcessed(queueID: queueID, result: (NWConnection.State.ready == networkState))
    }
}


So, without putting any code in the main code body (deployable app/package) I can simulate network conditions per unit test.


cheers,

That's one possible solution, but there may be some others.

let mockClient = AppClient(mockNetHandler: MockNetworkHandler())
  
(mockClient.netHandler as! MockNetworkHandler).websiteActive = false

Or

let mockHandler = MockNetworkHandler()
mockHandler.websiteActive = false
let mockClient = AppClient(mockNetHandler: mockHandler)


No need to define another initializer for MockNetworkHandler.

So the choices are


1. after assigning the mock handler to mockClient.netHandler, the value can be changed by force-casting it back to MockNetworkHandler. (your first above)

2. to set the value before assigning it to mockClient.netHandler, at which point netHandler is effectively cast to the NetworkHandler type. (additional initialisers, your second above)


All makes sense now.


thanks.