Daniel Fernandez Y
Daniel Fernandez Y

Reputation: 188

Cannot convert value of type 'MyEnum<T.Type>' to expected argument type 'MyEnum<_>'

I have a network layer working with generics and I'm using protocols so I can test it later. I have followed this tutorial https://medium.com/thecocoapps/network-layer-in-swift-4-0-972bf2ea5033

This is my Mock for testing:

import Foundation
@testable import TraktTest

class MockUrlSessionProvider: ProviderProtocol {

    enum Mode {
        case success
        case empty
        case fail
    }

    private var mode: Mode

    init(mode: Mode) {
        self.mode = mode
    }

    func request<T>(type: T.Type, service: ServiceProtocol, completion: @escaping (NetworkResponse<T>) -> Void) where T: Decodable {                
        switch mode {
        case .success: completion(NetworkResponse.success(T))
        case .empty: completion(.failure(.noData))
        case .fail: completion(.failure(.unknown("Error")))
        }

    }
}

I'm getting the error: Cannot convert value of type 'NetworkResponse<T.Type>' to expected argument type 'NetworkResponse<_>' in this line: completion(NetworkResponse.success(T))

If I send this to my completion success it compile: try? JSONDecoder().decode(T.self, from: data!) (dummy data that I created using encode and my model), but crash when get to my model because is nil despite I had encoded using JSONEncoder() with a correct model.

I think it works, because is the same logic that I use in my class that implements ProviderProtocol in my app:

final class URLSessionProvider: ProviderProtocol {

    private var session: URLSessionProtocol

    init(session: URLSessionProtocol = URLSession.shared) {
        self.session = session
    }

    func request<T>(type: T.Type, service: ServiceProtocol, completion: @escaping (NetworkResponse<T>) -> Void) where T: Decodable {
        let request = URLRequest(service: service)
        session.dataTask(request: request) { [weak self]  data, response, error in
            let httpResponse = response as? HTTPURLResponse
            self?.handleDataResponse(data: data, response: httpResponse, error: error, completion: completion)
        }.resume()
    }

    private func handleDataResponse<T: Decodable>(data: Data?, response: HTTPURLResponse?, error: Error?, completion: (NetworkResponse<T>) -> Void) {
        guard error == nil else { return completion(.failure(.unknown(error?.localizedDescription ?? "Error"))) }
        guard let response = response else { return completion(.failure(.unknown("no_response".localized()))) }

        switch response.statusCode {
        case 200...299:
            guard let data = data, let model = try? JSONDecoder().decode(T.self, from: data) else { return completion(.failure(.noData)) }
            completion(.success(model))
        default: completion(.failure(.unknown("no_response".localized())))
        }
    }

}

URLSessionProtocol is just a protocol which has a method dataTask same as the one in URLSession.shared (receive a URLRequest and returns Data, Response and Error in a completion).

My Network responses are a couple of enums:

enum NetworkResponse<T> {
    case success(T)
    case failure(NetworkError)
}

enum NetworkError {
    case unknown(String)
    case noData
}

My provider protocol just have a function to make the request using generics:

protocol ProviderProtocol {
    func request<T>(type: T.Type, service: ServiceProtocol, completion: @escaping(NetworkResponse<T>) -> Void) where T: Decodable
}

I don't think I need to use ServiceProtocol in my test because is to setup the request with endpoint, headers, body, id, etc. But this is the protocol I created:

typealias Headers = [String: String]
typealias Parameters = [String: Any]

protocol ServiceProtocol {
    func baseURL() -> URL
    var path: String? { get }
    var id: String? { get }
    var method: HTTPMethod { get }
    var task: Task { get }
    var headers: Headers? { get }
    var parametersEncoding: ParametersEncoding { get }
}

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
}

enum Task {
    case requestPlain
    case requestParameters(Parameters)
}

enum ParametersEncoding {
    case url
    case json
}

In my app, I have a class that implement ProviderProtocol and use a URLSession.shared to make the dataTask when some viewModel call the request with the appropiate model.

I'm use to make test with protocols and a specific model, but with generics is showing me that error. How can I achieve to have a mock provider using generics so I can test any viewModel who make a call to network using different kinds of models (stubs).

Upvotes: 0

Views: 969

Answers (1)

Cristik
Cristik

Reputation: 32870

The error occurs because NetworkResponse expects an instance of T, while the mock tries to provide the actual T.

So, you need to somehow provide an instance, however this cannot be generated by the mock as it doesn't have enough information about how to construct an instance.

I recommend injecting the success value from the outside, when creating the mock. You can do this either by making the mock class generic, or by making the Mode enum generic. Below is a sample implementation for the latter:

class MockUrlSessionProvider: ProviderProtocol {

    // making the enum generic, to support injecting the success value
    enum Mode<T> {
        case success(T)
        case empty
        case fail
    }

    // need to have this as `Any` to cover all possible T generic arguments
    private var mode: Any

    // however the initializer can be very specific
    init<T>(mode: Mode<T>) {
        self.mode = mode
    }

    func request<T>(type: T.Type, service: ServiceProtocol, completion: @escaping (NetworkResponse<T>) -> Void) where T: Decodable {
        // if the mock was not properly configured, do nothing
        guard let mode = mode as? Mode<T> else { return }
        // alternatively you force cast and have the unit test crash, this should help catching early configuration issues
        // let mode = mode as! Mode<T>

        switch mode {
        case let .success(value): completion(NetworkResponse.success(value))
        case .empty: completion(.failure(.noData))
        case .fail: completion(.failure(.unknown("Error")))
        }

    }
}

Upvotes: 1

Related Questions