天气或传感器应用把“升温十摄氏度”显示成“升温五十华氏度”,往往不是格式化问题,而是把温差当成绝对温度换算。在云端 Mac 上,可以先用独立 Swift 测试包固定计算契约,再接入 iOS 工程;不需要连接传感器,也不需要启动模拟器。
先把读数和变化量拆开
绝对温度转换同时涉及比例和原点偏移:零摄氏度对应三十二华氏度。温差只有比例变化:增加十摄氏度对应增加十八华氏度。Foundation 的 Measurement<UnitTemperature> 表达温度读数,不会根据变量名猜测它是不是变化量。
建议模型明确区分 reading 与 delta,接口文档注明单位。业务层统一保留未格式化数值,显示时才决定小数位。不要将“二十三点五度”这样的界面文本重新解析为计算输入。
温差允许为负;低于绝对零度的绝对温度应被拒绝。两个字段不能共用同一套范围校验。
下面的校验针对物理温度。若产品处理的是数学外推数据,应另外声明范围,而不是悄悄放宽入口。
建立不依赖界面的测试包
先在独立目录确认工具链并生成包,避免修改现有项目配置。命令要求已安装可用的 Xcode 命令行工具;测试使用当前选中的 Swift 工具链。
xcrun swift --version
workdir="$(mktemp -d)"
cd "$workdir"
swift package init --type library --name TemperatureChecks
保留生成的文件,再将下一节代码保存为 Tests/TemperatureChecksTests/TemperatureContractTests.swift。代码只依赖 Foundation 与 XCTest,不需要增加外部包。
独立包验证的是换算规则,不代表 iOS 显示已经通过验收。迁入项目后,应让测试调用生产实现,避免两份函数分别修改、测试始终检查旧副本。
用基准点和往返转换固定规则
绝对温度先检查有限值,再换算为开尔文检查物理下界。温差则通过“转换后的端点减去转换后的零点”消除偏移。这个写法只用于这里的温度线性换算,不要直接推广到任意单位模型。
import Foundation
import XCTest
private enum TemperatureError: Error {
case invalidReading
}
private func kelvin(
_ value: Double,
from unit: UnitTemperature
) throws -> Double {
guard value.isFinite else {
throw TemperatureError.invalidReading
}
let result = Measurement(value: value, unit: unit)
.converted(to: .kelvin).value
guard result.isFinite, result >= 0 else {
throw TemperatureError.invalidReading
}
return result
}
private func temperatureDelta(
_ value: Double,
from source: UnitTemperature,
to target: UnitTemperature
) -> Double {
let endpoint = Measurement(value: value, unit: source)
.converted(to: target).value
let origin = Measurement(value: 0, unit: source)
.converted(to: target).value
return endpoint - origin
}
final class TemperatureContractTests: XCTestCase {
func testReferencePoints() throws {
let freezing = Measurement(
value: 0,
unit: UnitTemperature.celsius
).converted(to: .fahrenheit).value
XCTAssertEqual(freezing, 32, accuracy: 1e-9)
XCTAssertEqual(
try kelvin(0, from: .celsius),
273.15,
accuracy: 1e-9
)
XCTAssertEqual(
try kelvin(-273.15, from: .celsius),
0,
accuracy: 1e-9
)
}
func testTemperatureDeltas() {
for value in [-10.0, 0.0, 10.0] {
XCTAssertEqual(
temperatureDelta(
value,
from: .celsius,
to: .fahrenheit
),
value * 1.8,
accuracy: 1e-9
)
}
}
func testRoundTrip() {
for value in [-40.0, 0.0, 37.0, 100.0] {
let original = Measurement(
value: value,
unit: UnitTemperature.celsius
)
let restored = original.converted(to: .fahrenheit)
.converted(to: .celsius)
XCTAssertEqual(
restored.value,
value,
accuracy: 1e-9
)
}
}
func testInvalidReadings() {
for value in [
Double.nan,
Double.infinity,
-Double.infinity,
-274.0
] {
XCTAssertThrowsError(
try kelvin(value, from: .celsius)
)
}
}
}
在包根目录运行 swift test。容差用于吸收浮点运算误差,不是业务允许误差。传感器精度应单独定义,不能靠把断言容差调大来隐藏错误。
往返测试也不能替代基准点:两个方向若犯了互相抵消的错误,往返仍可能通过。至少保留零点、负值、已知参考值和非法输入四类样本。温差入口还应自行拒绝非有限值;上面的辅助函数只展示换算,不承担输入验证。
将本地化与数值精度分开验收
把下面代码保存为包目录中的 format-temperature.swift,用 swift format-temperature.swift 执行。这里固定摄氏单位,避免区域偏好自动改变单位,让检查对象只剩显示规则。
import Foundation
for identifier in ["zh_CN", "en_US", "fr_FR"] {
let locale = Locale(identifier: identifier)
let formatter = MeasurementFormatter()
formatter.locale = locale
formatter.unitOptions = .providedUnit
formatter.unitStyle = .short
formatter.numberFormatter.locale = locale
formatter.numberFormatter.minimumFractionDigits = 1
formatter.numberFormatter.maximumFractionDigits = 1
let reading = Measurement(
value: 23.45,
unit: UnitTemperature.celsius
)
print(identifier, formatter.string(from: reading))
}
检查小数分隔符、单位符号和间距,但不要假定不同系统版本的整条字符串逐字相同。需要快照断言时,一并记录系统版本和区域标识。
温差显示不要直接交给绝对温度格式器隐式处理。先算出目标温差,再使用数字格式器及本地化资源明确标注“温差”。负零也应有产品规则:若舍入后显示为零,是否去掉负号,要在显示层统一处理。
接入工程时守住三个边界
第一,原始数据必须携带单位。未知单位不能默认按摄氏解释,否则错误数据仍可能落在合理范围内。协议中的未知枚举值应返回明确错误。
第二,阈值比较发生在统一单位、未舍入的数值上。显示为同一个数字的两条读数,不一定具有相同的报警结果。增加一个“数值不同但显示相同”的样本,可以防止界面重构影响业务判断。
第三,云端 Mac 上的 Foundation 测试通过后,还要在目标 iOS 运行环境检查格式和布局。小组件、图表轴标签、语音朗读应分别验收,不能只检查主页面。
最终保留测试结果、工具链版本和格式化输出。发生回归时,先判断是换算、输入契约还是显示变化,再修改对应层;不要用一个界面字符串断言包办全部正确性。
常见问题
为什么温差不能直接使用绝对温度的转换方法?
摄氏与华氏之间的绝对温度转换包含偏移量,温差转换只能应用比例。可分别转换温差值与源单位的零点,再相减;十摄氏度的温差对应十八华氏度的温差。
本地化温度字符串适合直接做固定文本断言吗?
只有同时固定系统版本、语言区域、单位选项和精度时才适合。常规门禁应先检查数值契约,显示层再检查指定单位、舍入及布局,避免把空格和符号变化误判为计算错误。
把下一次构建交给独享云端 Mac
MacFTP 提供运行原生 macOS 的 Apple Silicon 独享物理机,按天、周、月、季选择租期。先确认配置,再将现有工具链接入节点;实际可用状态以控制台实时返回为准。