Swift中如何Mock泛型类以完成单元测试
解决Swift泛型依赖Mock的问题
你遇到的报错是因为带associatedtype的协议不能直接用作变量类型——Swift需要明确协议的具体关联类型,才能确定变量的实际类型。下面给你两种可行的解决方案,帮你MockDependencyClass来测试ClassToTest:
方案一:调整协议,移除关联类型(推荐,适配当前场景)
既然你的ClassToTest里依赖的DependencyClass固定使用AnotherClass作为泛型参数,完全可以把协议的方法参数改成具体的AnotherClass,去掉关联类型,这样就能直接用协议作为变量类型了。
修改后的代码
1. 重新定义协议
protocol DependencyClassProtocol { func getData(from general: AnotherClass) -> AnyPublisher<Data, Error> }
2. 让DependencyClass遵守协议
因为ClassToTest用的是DependencyClass<AnotherClass>,可以直接给对应泛型约束的扩展实现协议:
class DependencyClass<General: AnotherClassProtocol> { func getData(from general: General) -> AnyPublisher<Data, Error> { // 原业务逻辑代码 } } // 让使用AnotherClass的DependencyClass遵守协议 extension DependencyClass where General == AnotherClass: DependencyClassProtocol {}
3. 修改ClassToTest的依赖注入
class ClassToTest: SomeProtocol { private let dependency: DependencyClassProtocol // 提供默认参数,不影响原有业务代码的调用 init(_ dependency: DependencyClassProtocol = DependencyClass<AnotherClass>()) { // 原初始化逻辑 self.dependency = dependency } }
4. 编写Mock类
import Combine class MockDependency: DependencyClassProtocol { // 用于验证方法是否被调用 var getDataCalled = false // 自定义返回数据或错误 var mockData: Data? var mockError: Error? func getData(from general: AnotherClass) -> AnyPublisher<Data, Error> { getDataCalled = true if let data = mockData { return Just(data) .setFailureType(to: Error.self) .eraseToAnyPublisher() } else if let error = mockError { return Fail(error: error) .eraseToAnyPublisher() } // 默认返回错误,避免测试卡死 return Fail(error: NSError(domain: "MockDependency", code: -1, userInfo: nil)) .eraseToAnyPublisher() } }
5. 测试示例
import XCTest import Combine class ClassToTestTests: XCTestCase { private var cancellables = Set<AnyCancellable>() func testDataFetchSuccess() { // 准备Mock依赖 let mockDependency = MockDependency() mockDependency.mockData = Data("test_content".utf8) let testInstance = ClassToTest(mockDependency) let expectation = self.expectation(description: "getData completes successfully") // 假设ClassToTest有个触发依赖调用的方法,比如fetchData() testInstance.fetchData() .sink( receiveCompletion: { completion in if case .finished = completion { expectation.fulfill() } else { XCTFail("Expected success, got failure") } }, receiveValue: { receivedData in // 验证返回数据正确 XCTAssertEqual(receivedData, Data("test_content".utf8)) // 验证方法被调用 XCTAssertTrue(mockDependency.getDataCalled) } ) .store(in: &cancellables) waitForExpectations(timeout: 1) } }
方案二:让ClassToTest成为泛型类(保留泛型灵活性)
如果未来你的ClassToTest需要更换DependencyClass的泛型参数,可以让ClassToTest变成泛型类,用带关联类型的协议作为泛型约束:
修改后的代码
1. 保留带关联类型的协议
protocol DependencyClassProtocol { associatedtype General: AnotherClassProtocol func getData(from general: General) -> AnyPublisher<Data, Error> } // DependencyClass自动遵守协议 class DependencyClass<General: AnotherClassProtocol>: DependencyClassProtocol { func getData(from general: General) -> AnyPublisher<Data, Error> { // 原业务逻辑代码 } }
2. 修改ClassToTest为泛型类
class ClassToTest<Dependency: DependencyClassProtocol>: SomeProtocol where Dependency.General == AnotherClass { private let dependency: Dependency init(_ dependency: Dependency = DependencyClass<AnotherClass>() as! Dependency) { // 原初始化逻辑 self.dependency = dependency } }
3. Mock类和测试示例
Mock类需要明确关联类型:
class MockDependency: DependencyClassProtocol { typealias General = AnotherClass var getDataCalled = false var mockData: Data? var mockError: Error? func getData(from general: AnotherClass) -> AnyPublisher<Data, Error> { getDataCalled = true // 同方案一的返回逻辑 if let data = mockData { return Just(data) .setFailureType(to: Error.self) .eraseToAnyPublisher() } else if let error = mockError { return Fail(error: error) .eraseToAnyPublisher() } return Fail(error: NSError(domain: "MockDependency", code: -1, userInfo: nil)) .eraseToAnyPublisher() } }
测试代码只需初始化泛型类时传入Mock实例:
func testDataFetchFailure() { let mockDependency = MockDependency() mockDependency.mockError = NSError(domain: "Test", code: 404, userInfo: nil) let testInstance = ClassToTest(mockDependency) let expectation = self.expectation(description: "getData completes with failure") testInstance.fetchData() .sink( receiveCompletion: { completion in if case .failure(let error) = completion { XCTAssertEqual((error as NSError).code, 404) expectation.fulfill() } else { XCTFail("Expected failure, got success") } }, receiveValue: { _ in XCTFail("Expected no data, got value") } ) .store(in: &cancellables) waitForExpectations(timeout: 1) }
内容的提问来源于stack exchange,提问作者Mohammad Azam
相关产品推荐
相关产品推荐

