如何用Sourcery为带关联类型的泛型网络协议生成Mock?
带关联类型的NetworkHandling协议Mock自动生成优化方案
核心问题拆解
你遇到的泛型作用域报错,本质是带关联类型的协议(比如NetworkHandling)无法直接生成单态Mock类——因为每个请求类型对应唯一的响应类型,默认模板生成的代码没法兼容多组请求-响应的绑定。下面给你两种可行的优化方案,基于Sourcery模板实现。
方案一:类型擦除式Mock(兼容任意请求类型)
1. 模板核心逻辑
通过一个私有的类型擦除容器,把不同请求-响应类型的处理器统一存储,用ObjectIdentifier做类型标识(和你手动实现的逻辑对齐)。
2. Sourcery模板代码
{% for protocol in types.protocols where name contains "NetworkHandling" %} public class Mock{{ protocol.name }}: {{ protocol.name }} { // 存储所有擦除后的请求处理器 private var handlers: [AnyRequestHandler] = [] // 实现协议要求的handle方法 public func handle<T: NetworkRequest>(_ request: T) async throws -> T.Response { // 匹配当前请求类型的处理器 guard let matchedHandler = handlers.first(where: { $0.requestTypeID == ObjectIdentifier(T.self) }) else { throw MockError.unhandledRequest(T.self) } // 强制转换是安全的,因为注册时已经绑定了类型 return try matchedHandler.handler(request) as! T.Response } // 对外暴露的注册方法,用来设置Mock响应 public func registerMock<T: NetworkRequest>(for requestType: T.Type, handler: @escaping (T) throws -> T.Response) { let erasedHandler = AnyRequestHandler( requestTypeID: ObjectIdentifier(requestType), handler: { input in try handler(input as! T) } ) // 先移除同类型的旧处理器,避免重复 handlers.removeAll(where: { $0.requestTypeID == ObjectIdentifier(requestType) }) handlers.append(erasedHandler) } } // 私有类型擦除容器 private struct AnyRequestHandler { let requestTypeID: ObjectIdentifier let handler: (Any) throws -> Any } // Mock错误定义 enum MockError: Error { case unhandledRequest(Any.Type) } {% endfor %}
3. 使用方式
生成代码后,你可以这样设置Mock响应:
let mockNetwork = MockNetworkHandling() // 注册某个请求的Mock响应 mockNetwork.registerMock(for: UserFetchRequest.self) { request in return User(id: 1, name: "Mock User") }
方案二:类型安全的请求-响应绑定(无强制转换)
如果想彻底避免强制类型转换的风险,可以让Sourcery遍历所有实现NetworkRequest的具体类型,为每个类型生成单独的处理器存储和注册方法。
1. Sourcery模板代码
{% for protocol in types.protocols where name contains "NetworkHandling" %} public class Mock{{ protocol.name }}: {{ protocol.name }} { // 为每个请求类型生成单独的处理器变量 {% for requestType in types.implementing["NetworkRequest"] %} private var {{ requestType.name | lowerFirst }}Handler: (({{ requestType.name }}) throws -> {{ requestType.name }}.Response)? {% endfor %} // 实现协议的handle方法,逐个匹配请求类型 public func handle<T: NetworkRequest>(_ request: T) async throws -> T.Response { {% for requestType in types.implementing["NetworkRequest"] %} if let concreteRequest = request as? {{ requestType.name }}, let handler = {{ requestType.name | lowerFirst }}Handler { return try handler(concreteRequest) as! T.Response } {% endfor %} throw MockError.unhandledRequest(T.self) } // 为每个请求类型生成类型安全的注册方法 {% for requestType in types.implementing["NetworkRequest"] %} public func register{{ requestType.name }}Handler(handler: @escaping ({{ requestType.name }}) throws -> {{ requestType.name }}.Response) { self.{{ requestType.name | lowerFirst }}Handler = handler } {% endfor %} } enum MockError: Error { case unhandledRequest(Any.Type) } {% endfor %}
2. 优势与局限
- 优势:完全类型安全,没有强制转换,编译期就能发现类型不匹配的问题
- 局限:需要提前让Sourcery扫描到所有实现
NetworkRequest的类型,新增请求类型后需要重新生成Mock代码
关键注意点
- 不要在Mock类的顶层声明泛型参数,把泛型约束放在方法层面,避免作用域冲突
- 两种方案都解决了“单个Mock类兼容多请求类型”的问题,前者灵活,后者更安全,根据你的需求选择
- 生成代码时确保Sourcery能正确识别
NetworkRequest协议和它的实现类
内容的提问来源于stack exchange,提问作者Lucas van Dongen
相关产品推荐
相关产品推荐

