【问题标题】:How can I pass a dependancy to a URLProtocol subclass?如何将依赖项传递给 URL 协议子类?
【发布时间】:2020-10-24 06:59:00
【问题描述】:

我一直在开发一个拦截器,用于在我的应用程序中将身份验证标头添加到网络请求中。

final class AuthInterceptor: URLProtocol {


    private var token: String = "my.access.token"   
    private var dataTask: URLSessionTask?
    private struct UnexpectedValuesRepresentationError: Error { }
    
    override class func canInit(with request: URLRequest) -> Bool {
        guard URLProtocol.property(forKey: "is_handled", in: request) as? Bool == nil else { return false }

        return true
        
        //return false // URL Loading System will handle the request using the system’s default behavior
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { return }
        URLProtocol.setProperty(true, forKey: "is_handled", in: mutableRequest)
        mutableRequest.addValue(token, forHTTPHeaderField: "Authorization")
        
        dataTask = URLSession.shared.dataTask(with: mutableRequest as URLRequest) { [weak self] data, response, error in
            guard let self = self else { return }
            if let error = error {
                self.client?.urlProtocol(self, didFailWithError: error)
            } else if let data = data, let response = response {
                self.client?.urlProtocol(self, didLoad: data)
                self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            } else {
                self.client?.urlProtocol(self, didFailWithError: UnexpectedValuesRepresentationError())
            }
            self.client?.urlProtocolDidFinishLoading(self)
        }
        
        dataTask?.resume()
    }
    
    override func stopLoading() {
        dataTask?.cancel()
        dataTask = nil
    }
}

如您所见,我目前只是使用private var token: String = "my.access.token" 来模拟令牌。我想介绍一个TokenLoader,它将从它的缓存中获取我的令牌。

由于 URL 加载系统将根据需要初始化我的协议实例,我不确定如何注入它。目前注册使用:URLProtocol.registerClass(AuthInterceptor.self)

想象一下我有一个这样的界面 -

public typealias LoadTokenResult = Result<String, Error>
public protocol TokenLoader {
  func load(_ key: String, completion: @escaping (LoadTokenResult) -> Void)
}

我想确保这是可测试的,所以我希望能够在测试用例中存根它或使用间谍。

我怎样才能做到这一点?

【问题讨论】:

  • 你成功了吗?

标签: ios swift dependency-injection urlsession nsurlprotocol


【解决方案1】:

正如所见AuthInterceptor 不依赖于实例级别的TokenProvider,因此您可以注入类级别的依赖项并使用共享的TokenProvider 来提取/加载令牌(例如,取决于URL)。这提供了注入缓存令牌提供程序以轻松进行测试的可能性

final class AuthInterceptor: URLProtocol {

    static var tokenProvider: TokenLoader?  // << inject here

    // initial value, will be loaded from to tokenProvider result
    private var token: String?  

    // ... other code
    
    // somewhere inside when needed, like
    Self.tokenProvider?.load(key) { [weak self] result in
       self?.token = try? result.get()

       // proceed with token if present ...
    }
}

所以在 UT 场景中,您可以执行类似的操作

override func setUp {
   AuthInterceptor.tokenProvider = MockTokenProvider()
}

override func tearDown {
   AuthInterceptor.tokenProvider = nil // or some default if needed
}

【讨论】:

    【解决方案2】:

    一种可能的选择是为此使用泛型。

    protocol TokenProvider {
        static var token: String {get}
    }
    
    class MockTokenProvider: TokenProvider {
        static var token: String = "my.access.token"
    }
    

    然后像这样改变 AuthInterceptor:

        final class AuthInterceptor<T: TokenProvider>: URLProtocol {
          private var token: String = T.token
        ....
        ...
    }
    

    然后像这样注册:

    URLProtocol.registerClass(AuthInterceptor<MockTokenProvider>.self)
    

    上面是在操场上编译的,不过还没有在实际项目中尝试过..

    【讨论】:

    • 这肯定会增加可测试性。但是为了我自己,我对问题的“通过依赖”部分更感兴趣。我希望能够传递一个类的实例以便能够获取我的访问令牌。或许这需要另一个问题。
    猜你喜欢
    • 1970-01-01
    • 2021-01-02
    • 2019-07-26
    • 1970-01-01
    • 2016-10-08
    • 2017-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多