进度条偶尔不刷新,统计图突然出现空白,问题可能不是界面线程,而是上游传入了 NaN。把传感器读数除以量程、把完成量除以总量,都可能产生非法结果。在 ArmShell 云端 Mac 上,可以先把这层计算拆成独立 Swift 包,用边界测试挡住异常,再接入 iOS 工程。
先写数值契约,而不是先调容差
本文只处理一种明确业务:把非负读数归一化到零至一。输入必须有限,量程必须大于零,结果必须有限且不越界;非法数据返回错误,不自动截断。零读数合法,负零统一输出为正零。
这里有一个容易漏掉的条件:value < 0 || value > 1 不能排除 NaN。NaN 的有序比较都返回 false,程序可能继续把它当作有效结果。
有限性、业务范围、近似相等是三层不同的检查。扩大测试容差,不能修复非法输入。
先明确错误处理策略也很重要:界面可以显示“数据不可用”,但不能把失败偷偷转换为零,否则用户无法区分真实零值与读取失败。
建立不依赖模拟器的测试包
先确认命令行工具可用,再在临时目录创建包。以下操作不修改应用工程,也不需要启动图形会话:
xcrun swift --version
root="$(mktemp -d)"
cd "$root"
swift package init --type library --name NumericBoundary
将 Sources/NumericBoundary/NumericBoundary.swift 的内容替换为:
public enum LevelError: Error, Equatable {
case invalidInput
case invalidResult
}
public func normalizedLevel(
_ raw: Double,
scale: Double
) throws -> Double {
guard raw.isFinite, scale.isFinite,
raw >= 0, scale > 0 else {
throw LevelError.invalidInput
}
let ratio = raw / scale
guard ratio.isFinite, ratio >= 0, ratio <= 1 else {
throw LevelError.invalidResult
}
return ratio == 0 ? 0.0 : ratio
}
不能只验证输入。两个有限数相除,仍可能得到无穷值,例如极大的分子除以极小的正分母。另一方面,超出量程的有限结果也应失败,不能依赖界面组件替计算层兜底。
这段实现允许正常浮点舍入;若业务还要求最小有效读数,需要另设阈值。极小正数相除可能下溢为零,本例接受它,不把“有限”误写成“完全无精度损失”。
用反例覆盖异常路径
将默认测试文件内容替换为以下代码。每个案例都检查错误类别,避免函数因其他原因抛错时误判通过:
import XCTest
@testable import NumericBoundary
final class NumericBoundaryTests: XCTestCase {
func testInvalidInputs() {
let cases: [(Double, Double)] = [
(.nan, 1),
(.infinity, 1),
(-.infinity, 1),
(1, .nan),
(1, .infinity),
(1, -.infinity),
(-1, 1),
(1, 0),
(1, -0.0),
(1, -1)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidInput)
}
}
}
func testInvalidResults() {
let cases: [(Double, Double)] = [
(2, 1),
(.greatestFiniteMagnitude, .leastNormalMagnitude)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidResult)
}
}
}
func testValidBoundaries() throws {
XCTAssertEqual(try normalizedLevel(0, scale: 1), 0)
XCTAssertEqual(try normalizedLevel(1, scale: 1), 1)
XCTAssertEqual(
try normalizedLevel(1, scale: 10),
0.1,
accuracy: 1e-12
)
let zero = try normalizedLevel(-0.0, scale: 1)
XCTAssertEqual(zero, 0)
XCTAssertEqual(zero.sign, .plus)
}
}
负零在普通相等比较中等于正零,因此必须额外检查 sign,才能证明归一化策略确实生效。异常案例也不要用“结果不等于 NaN”断言:NaN 连自身都不相等,这种测试没有证明力。
容差必须跟业务尺度绑定
示例输出范围固定为零至一,使用绝对误差比较比较直观。1e-12 是这段小计算的测试选择,不是所有传感器、图形或统计任务的通用精度标准。
如果计算值跨度很大,应先确认两个值都有限,再按业务采用绝对误差与相对误差的组合:
abs(actual - expected)
<= max(absoluteTolerance,
relativeTolerance * max(abs(actual), abs(expected)))
两个容差本身也应有限且非负。对极端量级,差值或右侧乘法可能溢出,不能把公式直接视作通用安全库。
零附近主要由绝对误差控制,大数附近主要由相对误差控制。测试代码应说明阈值来源,例如设备分辨率或算法误差预算,而不是一直放宽到测试通过。也不要提前格式化成小数字符串再比较,那会掩盖真实差异。
接入流水线并证明门禁有效
在包目录执行:
swift test
swift test -c release
调试和发布配置都要跑。生产校验使用 guard 与抛错,而不是可能在优化构建中被移除的 assert。流水线必须保留命令退出码,测试失败就停止后续交付。
随后做一次破坏验证:临时去掉输入检查中的 raw.isFinite,确认 NaN 案例因错误类别变化而失败,再恢复代码。这样能证明测试真正覆盖了入口,而不只是执行过函数。
接入应用时,应让业务代码调用同一份实现,不要在测试包里维护一个副本。记录工具链版本、提交标识和失败案例即可,不必上传真实用户读数。最终验收标准是:异常输入明确失败、合法边界稳定通过、容差有来源,并且发布配置下仍执行相同契约。
常见问题
只判断数值是否小于零或大于一,能挡住 NaN 吗?
不能。NaN 与零、一进行有序比较时都返回 false,必须先用 isFinite 排除 NaN 和正负无穷,再做范围校验。
输入都是有限数,运算结果还需要检查吗?
需要。有限数相除仍可能溢出为无穷值,运算结果必须再次检查 isFinite,并核对业务允许的范围。