You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 23:39:20