Swift中如何对带有依赖的函数进行单元测试?
嘿,我太懂这种光听概念摸不着头脑的感觉了!刚接触mock和stub的时候,我也是对着名词发呆,直到看了实际代码示例才恍然大悟。结合你提到的私有函数测试场景,我给你拆解一下,保证看完就会用!
先搞懂Mock vs Stub:一句话区分
- Stub(存根):给你的测试提供「固定输入/返回值」的假实现,核心是隔离依赖。比如你要测试的函数依赖数据库返回的数据,Stub就直接返回你预设好的假数据,不用真的连数据库。
- Mock(模拟):不仅能提供假实现,还会记录和验证交互行为——比如检查某个依赖方法有没有被调用、调用了几次、传的参数对不对。
结合你的场景写示例(Swift为例)
假设你的setOverviewValues是一个ViewModel里的私有函数,依赖CDPRewards(Core Data实体)和displayMode,内部会调用UI更新方法。我们一步步来改造和测试它。
第一步:抽象依赖,方便替换
首先把CDPRewards的核心属性抽象成协议,这样我们就能用Stub来模拟它:
// 定义协议,提取你需要用到的属性 protocol RewardsProtocol { var totalPoints: Int { get } var weeklyEarnings: [Int] { get } } // 让真实的Core Data实体遵守这个协议 extension CDPRewards: RewardsProtocol {}
第二步:写Stub类,模拟CDPRewards
这个Stub类会返回我们预设的值,完全隔离真实的Core Data依赖:
class StubRewards: RewardsProtocol { // 自定义属性,方便测试不同场景 var totalPoints: Int var weeklyEarnings: [Int] // 给默认值,测试时可以按需修改 init(totalPoints: Int = 100, weeklyEarnings: [Int] = [20,30,50]) { self.totalPoints = totalPoints self.weeklyEarnings = weeklyEarnings } }
第三步:用Mock验证交互
如果你的setOverviewValues会调用UI更新方法,我们可以写一个Mock类来记录这些调用,确保逻辑正确:
@testable import YourAppName // Mock ViewModel,用来跟踪UI更新方法的调用情况 class MockOverviewViewModel: OverviewViewModel { // 记录是否调用了对应的方法 var didCallCompactUpdate = false var didCallFullUpdate = false // 记录传入的参数,方便验证 var compactUpdateParams: (total: String, points: Int)? var fullUpdateParams: (earnings: [Int], total: String)? // 重写UI更新方法,替换成记录逻辑 override func updateCompactView(total: String, points: Int) { didCallCompactUpdate = true compactUpdateParams = (total, points) } override func updateFullView(earnings: [Int], total: String) { didCallFullUpdate = true fullUpdateParams = (earnings, total) } // 暴露私有函数给测试(解决private无法访问的问题) func testableSetOverviewValues(weeklyData: RewardsProtocol, formattedTotal: String, displayMode: DisplayMode) { #if DEBUG self.setOverviewValues(weeklyData: weeklyData as! CDPRewards, formattedTotal: formattedTotal, displayMode: displayMode) #endif } }
第四步:写测试用例
现在我们可以用Stub提供假数据,用Mock验证逻辑:
class OverviewViewModelTests: XCTestCase { func testCompactMode_UpdatesCorrectView() { // 1. 创建Stub的假数据 let stubRewards = StubRewards(totalPoints: 150, weeklyEarnings: [30,40,80]) // 2. 创建Mock ViewModel let mockVM = MockOverviewViewModel() // 3. 调用测试用的函数 mockVM.testableSetOverviewValues(weeklyData: stubRewards, formattedTotal: "$15.00", displayMode: .compact) // 4. 验证结果:是否正确调用了compact模式的更新方法 XCTAssertTrue(mockVM.didCallCompactUpdate) XCTAssertEqual(mockVM.compactUpdateParams?.total, "$15.00") XCTAssertEqual(mockVM.compactUpdateParams?.points, 150) // 验证没有调用full模式的方法 XCTAssertFalse(mockVM.didCallFullUpdate) } func testFullMode_UpdatesCorrectView() { let stubRewards = StubRewards(weeklyEarnings: [10,20,30,40]) let mockVM = MockOverviewViewModel() mockVM.testableSetOverviewValues(weeklyData: stubRewards, formattedTotal: "$10.00", displayMode: .full) XCTAssertTrue(mockVM.didCallFullUpdate) XCTAssertEqual(mockVM.fullUpdateParams?.earnings, [10,20,30,40]) XCTAssertEqual(mockVM.fullUpdateParams?.total, "$10.00") XCTAssertFalse(mockVM.didCallCompactUpdate) } }
关于私有函数测试的小技巧
如果你的函数是private的,Swift的@testable import无法直接访问,这里有两个办法:
- 改成internal访问级别:默认就是internal,不用加关键字,测试target通过
@testable import就能访问。 - 用扩展暴露给测试:在主target里加一个只在Debug模式下编译的扩展:
extension OverviewViewModel { #if DEBUG func exposeSetOverviewValues(weeklyData: CDPRewards, formattedTotal: String, displayMode: DisplayMode) { setOverviewValues(weeklyData: weeklyData, formattedTotal: formattedTotal, displayMode: displayMode) } #endif }
最后再划个重点
- 用Stub解决「依赖不好造」的问题:比如数据库、网络请求、第三方SDK,都可以用Stub替换。
- 用Mock解决「逻辑是否正确触发了依赖」的问题:比如验证按钮点击后是否调用了API,或者某个状态下是否更新了UI。
- 核心思路是隔离依赖:把测试目标函数和它的依赖分开,只测试函数本身的逻辑。
内容的提问来源于stack exchange,提问作者Erent
相关产品推荐
相关产品推荐

