Cuckoo测试泛型函数Stub难题及解决方案
2024-12-31 23:58:43
使用 Cuckoo 测试泛型函数时无法进行 Stub
使用 Cuckoo 进行单元测试时,对泛型函数进行 Stub 有时会遇到问题。特别是当函数签名包含泛型类型参数时,编译器可能无法自动推断类型,导致测试无法正确执行。本文讨论这类问题,并提供切实可行的解决方案。
问题分析
考虑一个使用泛型方法 execute
的 NetworkManager
协议。
protocol NetworkManager {
func execute<R: NetworkRequest, T: Decodable>(
with request: R,
parameters: [NetworkParameter]?
) async throws -> T
}
如测试示例所示,我们期望使用Cuckoo
来对该函数进行打桩(stubbing)。在 stub(mockNetworkManager)
部分尝试使用 when(stub.execute(with: any(PARRequest.self), parameters: any())).thenThrow(expectedError)
对函数进行stub 时,报错信息提示 “Generic parameter ‘T’ could not be inferred”, 表明 Cuckoo 无法推断出 execute
方法的 T
类型。这是由于 Cuckoo
的类型推断机制对泛型方法的推断较为有限。在生成测试桩代码的时候, 需要明确指定泛型类型。
解决方案
为了解决这个问题,需要显式地提供泛型类型参数给 Cuckoo。以下是两种有效的方案。
方案一:指定明确的返回值类型
原理:
此方案直接使用函数返回的特定类型(在本例中是AuthorizationResponseDTO
)明确指定了泛型方法,以便 Cuckoo 正确进行类型推断。在实际开发过程中, 几乎每一个API请求都有自己特定的DTO返回值, 在绝大多数情况下我们可以提前预知并且定义好的返回模型. 所以这是一个比较直接并且推荐使用的方案。
步骤:
- 将
.execute
调用时要期望返回的类型 (比如AuthorizationResponseDTO.self
) 一并进行指定,以明确T
的类型。
代码示例:
func testExecutePARRefreshURL_ThrowsError() async {
let testURL = "https://test.com"
let expectedError = NSError(domain: "TestErrorDomain", code: 1, userInfo: nil)
let request = PARRequest(baseURL: testURL)
stub(mockNetworkManager) { stub in
when(stub.execute(with: any(PARRequest.self), parameters: any()) as AuthorizationResponseDTO)
.thenThrow(expectedError)
}
do {
_ = try await sut.executePARRefreshURL(with: testURL)
XCTFail("Expected error but got success")
} catch let error as NSError {
XCTAssertEqual(error.domain, expectedError.domain)
}
verify(mockNetworkManager).execute(with: request, parameters: nil)
}
分析:
上述代码中, as AuthorizationResponseDTO
部分明确了泛型类型参数 T
的具体类型为 AuthorizationResponseDTO
, 从而解决了Cuckoo无法进行类型推断的问题。Cuckoo 将会使用这个信息生成对应的 Stub 代码,测试逻辑就可以顺利进行。
方案二:自定义一个辅助类型和扩展方法
原理:
创建一个封装类型 AnyDecodable
和 AnyNetworkRequest
,可以辅助我们进行 Cuckoo
类型定义。此方案稍微繁琐一些,当 execute
方法的返回模型无法被准确预知时, 使用该方案进行Stub. AnyDecodable
和 AnyNetworkRequest
起到抹平 T
以及R
类型差异性的作用。
步骤:
- 定义一个类型抹平的
AnyDecodable
结构体, 并为其提供构造器和对应的协议实现:
struct AnyDecodable: Decodable {}
protocol AnyNetworkRequest: NetworkRequest {
associatedtype Response = AnyDecodable
func asAnyNetworkRequest() -> AnyNetworkRequest
}
extension AnyNetworkRequest{
func asAnyNetworkRequest() -> AnyNetworkRequest { self }
}
- 使需要
stub
的NetworkRequest
都实现AnyNetworkRequest
协议:
struct PARRequest: NetworkRequest, AnyNetworkRequest {
...
func asAnyNetworkRequest() -> AnyNetworkRequest { self }
typealias Response = AuthorizationResponseDTO
}
- 在测试方法中,利用
AnyNetworkRequest
和AnyDecodable
作为参数执行stub
:
func testExecutePARRefreshURL_ThrowsError() async {
let testURL = "https://test.com"
let expectedError = NSError(domain: "TestErrorDomain", code: 1, userInfo: nil)
let request = PARRequest(baseURL: testURL)
stub(mockNetworkManager) { stub in
when(stub.execute(with: any(AnyNetworkRequest.self), parameters: any()) as AnyDecodable)
.thenThrow(expectedError)
}
do {
_ = try await sut.executePARRefreshURL(with: testURL)
XCTFail("Expected error but got success")
} catch let error as NSError {
XCTAssertEqual(error.domain, expectedError.domain)
}
verify(mockNetworkManager).execute(with: request, parameters: nil)
}
分析:
通过自定义类型,可以减少类型擦除操作带来的代码维护成本。在执行 stub
的时候使用 any(AnyNetworkRequest.self)
和 AnyDecodable
类型定义,能使 Cuckoo 准确生成对应的 Stub 代码。asAnyNetworkRequest()
扩展方法可以帮助 NetworkRequest
快速转化为 AnyNetworkRequest
类型。
安全提示
- 当测试
thenThrow
场景时, 保证错误处理代码覆盖到了各种可能的异常类型。 - 始终验证交互行为,确保 mock 对象的方法调用与预期一致。 使用
verify
来验证 mock 对象上是否有按照预期执行了相应方法调用。 - 在真实项目中,保证在执行 Cuckoo stub 方法前定义好各种需要用到的协议。确保生成 mock 文件中的函数签名参数正确。
总结
使用 Cuckoo 测试泛型方法时遇到 “Generic parameter could not be inferred” 的错误通常是因为 Cuckoo 无法自动推断出泛型类型。通过明确指定泛型参数类型, 或者自定义 AnyNetworkRequest
这种可以抹除类型差异性的泛型类型。这两种方案都可以解决这一问题。选择哪种方案取决于实际场景和开发团队的偏好。优先考虑使用类型更为明确方案,能够更好地表达意图并且能使代码更简洁易懂。