Mocking a class with generic parameter function inside Swift

Viewed 846

I am trying to unit test simple HttpClient behaviour. For that I have created an GenericHttpClientInterface protocol and concrete class GenericHttpClient that implements this protocol.

protocol GenericHttpClientInterface {
    func makeRequest<T: Decodable>(request: URLRequest) -> Observable<T>
}

class GenericHttpClient: GenericHttpClientInterface {
    func makeRequest<T: Decodable>(request: URLRequest) -> Observable<T> {
        return URLSession.shared.rx.data(request: request).jsonDecode(to: T.self)
    }
}

What I wanted to achieve is to mock that class :

class MockHttpClient: GenericHttpClientInterface {
    var invokedMakeRequestCount = 0
    var invokedMakeRequestParameters: (request: URLRequest, Void)?
    var stubbedMakeRequestResult: Observable<Any>!
    func makeRequest<T: Decodable>(request: URLRequest) -> Observable<T> {
        invokedMakeRequestCount += 1
        invokedMakeRequestParameters = (request, ())
        return stubbedMakeRequestResult as! Observable<T>;
    }
}

What gives ma a problem is that the method I am mocking has generic parameter T inside which is a class that the request will be decoded to. I don't know this parameter until I call this function so basically in MockHttpClient class for a property that stores stub data for makeRequest I've created:

stubbedMakeRequestResult: Observable<Any>

and after returning it I'm trying to cast that to result type Observable. This gives me an warning

Cast from 'Observable<Any>?' to unrelated type 'Observable<T>' always fails

and in consequence

Thread 1: signal SIGABRT.

Any idea how to stub that data?

Example test that creates SIGABRT:

class GenericHttpTest: XCTestCase {
    
    var sut: Repository!
    var mockHttpClient: MockHttpClient!
    
    override func setUp() {
        mockHttpClient = MockHttpClient()
        sut = Repository(httpClient: mockHttpClient)
    }
        
    let test_mocked_data_stub = DataModelStruct(args: DataModelStruct.InsideModelStruct(foo1: "bar"))

    func test_should_return_mocked_data_from_mock_http_client() {
        mockHttpClient.stubbedMakeRequestResult = Observable.just(test_mocked_data_stub)
        
        let response = try! sut.getFooBar().toBlocking().first()

        XCTAssertEqual(response, test_mocked_data_stub)
    }
}
1 Answers

Due to the fact that generics are invariant in Swift, Observable<Any> will never ever be convertible to Observable<T>, unless T is Any. This is what cause your crash, as when you assign the value to stubbedMakeRequestResult, the concrete Observable is converted to Observable<Any>, and there's no turning point from here.

What you can do to avoid this is to make stubbedMakeRequestResult an Any, as this will not make any conversions behind the scenes. A small problem is that you loose the type safety and inference, but you can fix this stubbing via a function:

class MockHttpClient: GenericHttpClientInterface {
    ...
    var stubbedMakeRequestResult: Any!
    
    func stubMakeRequestResult<T>(_ result: Observable<T>) {
        stubbedMakeRequestResult = result
    }
    ...
}
Related