Search code examples
swiftgenericsxctest

How to capture arguments from Swift generic function


I have a Swift protocol that defines a generic function for making network requests. It looks something like this:

protocol TaskManagerProtocol {
    func run<V>(
        task: Task<V>,
        completion: @escaping (Result<V, Error>) -> Void
    )
}

This works great in production code, letting me make requests that receive different types of results. So far, so good.

The problem: For unit testing, I want a Test Spy that captures the arguments, especially the closure. The spy looks like this:

class TaskManagerSpy<Value>: TaskManagerProtocol {
    var callCount = 0
    var task: [Task<Value>] = []
    var completion: [(Result<Value, Error>) -> Void] = []

    func run<V>(
        task: Task<V>,
        completion: @escaping (Result<V, Error>) -> Void
    ) {
        guard V.self == Value.self else {
            fatalError("run<V> doesn't match init<Value>")
        }
        callCount += 1
        self.task.append(task)
        self.completion.append(completion)
    }
}

My intention is to have test code instantiate TaskManagerSpy<SomeType> and inject that into the System Under Test, which calls run<V>(task:completion:). But as it stands, the append() calls fail to compile:

Cannot convert value of type 'Task<V>' to expected argument type 'Task<Value>'

and the same for the closure. If I comment out these lines, the test succeeds without triggering the fatal error.

Question: I have demonstrated that the types are the same. Is there a way to coerce one type into the other, so that I can capture the arguments?


Solution

  • Since, as you said, you've already asserted V.self == Value.self, you ought to be able to force cast task:

    self.task.append(task as! Task<Value>)

    Same thing for completion:

    self.completion.append(completion as! ((Result<Value, Error>) -> Void))