Если приложение погоды или приложение для работы с датчиками показывает «потепление на пятьдесят градусов Фаренгейта» вместо «потепления на десять градусов Цельсия», причина обычно не в форматировании: разность температур пересчитали как абсолютное значение. На облачном Mac можно сначала закрепить правила расчёта в отдельном пакете тестов Swift, а затем подключить их к проекту iOS. Для этого не нужны ни подключённый датчик, ни запущенный симулятор.
Разделите показания и изменения температуры
При преобразовании абсолютной температуры учитываются и масштаб, и смещение начала отсчёта: ноль градусов Цельсия соответствует тридцати двум градусам Фаренгейта. Для разности температур меняется только масштаб: повышение на десять градусов Цельсия соответствует повышению на восемнадцать градусов Фаренгейта. Тип Measurement<UnitTemperature> из Foundation представляет показание температуры и не определяет по имени переменной, хранится ли в ней изменение.
В модели стоит явно разделить 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))
}
Проверьте десятичный разделитель, обозначение единицы и пробелы, но не рассчитывайте на посимвольное совпадение всей строки в разных версиях системы. Если нужны проверки по снимкам, вместе с ними фиксируйте версию системы и идентификатор локали.
Не передавайте разность температур форматтеру абсолютной температуры в расчёте на неявную обработку. Сначала вычислите разность в нужных единицах, затем используйте числовой форматтер и локализованные ресурсы, явно обозначив «разность температур». Для отрицательного нуля также нужно правило продукта: если после округления отображается ноль, решение об удалении минуса должно единообразно применяться на уровне отображения.
При интеграции в проект соблюдайте три границы ответственности
Во-первых, исходные данные обязательно должны содержать единицу измерения. Неизвестную единицу нельзя по умолчанию считать градусами Цельсия: ошибочное значение всё равно может попасть в правдоподобный диапазон. Неизвестные значения перечисления в протоколе должны приводить к явной ошибке.
Во-вторых, сравнивать значения с порогами нужно в единых единицах измерения и до округления. Два показания, отображаемые одинаковым числом, не обязательно одинаково влияют на срабатывание оповещения. Добавьте пример, в котором числовые значения различаются, а отображение совпадает: он поможет не допустить, чтобы переработка интерфейса изменила бизнес-логику.
В-третьих, после успешного прохождения тестов Foundation на облачном Mac всё равно нужно проверить форматирование и компоновку в целевой среде выполнения iOS. Виджеты, подписи осей графиков и голосовое озвучивание проверяйте отдельно, не ограничиваясь главным экраном.
Сохраняйте результаты тестов, версию инструментария и форматированный вывод. При регрессии сначала определите, что изменилось: преобразование, контракт входных данных или отображение. Затем исправляйте соответствующий слой, а не пытайтесь подтвердить всю корректность одной проверкой строки интерфейса.
Часто задаваемые вопросы
Почему разность температур нельзя преобразовывать как абсолютную температуру?
Преобразование абсолютных значений включает смещение шкалы. Для разности нужно преобразовать её значение и ноль исходной шкалы, затем вычесть результаты. Разность в десять градусов Цельсия равна восемнадцати градусам Фаренгейта.
Стоит ли сравнивать локализованную строку с фиксированным текстом?
Только при закреплённых версии системы, локали, единице и точности. Числовые проверки лучше отделить от проверок округления, символов и расположения текста.
Поручите следующую сборку выделенному облачному Mac
MacFTP предоставляет выделенные физические компьютеры на Apple Silicon с оригинальной macOS. Выберите срок аренды: день, неделя, месяц или квартал. Сначала проверьте конфигурацию, затем подключите к узлу привычные инструменты. Фактический статус доступности отображается в панели управления.