iOS-Temperaturen auf dem Cloud-Mac testen: Werte, Differenzen und Anzeige

iOS-Temperaturen auf dem Cloud-Mac testen: Werte, Differenzen und Anzeige

iOS-Temperaturen auf dem Cloud-Mac testen: Werte, Differenzen und Anzeige

Wenn eine Wetter- oder Sensor-App aus „Erwärmung um zehn Grad Celsius“ eine „Erwärmung um fünfzig Grad Fahrenheit“ macht, liegt das meist nicht an der Formatierung. Vielmehr wurde eine Temperaturdifferenz wie eine absolute Temperatur umgerechnet. Auf einem Cloud-Mac lassen sich die Berechnungsregeln zunächst mit einem eigenständigen Swift-Testpaket absichern und anschließend in das iOS-Projekt übernehmen. Dafür müssen weder Sensoren angeschlossen noch der Simulator gestartet werden.

Messwerte und Differenzen klar trennen

Bei der Umrechnung absoluter Temperaturen ändern sich sowohl der Maßstab als auch der Nullpunkt: Null Grad Celsius entsprechen zweiunddreißig Grad Fahrenheit. Bei Temperaturdifferenzen ändert sich dagegen nur der Maßstab: Ein Anstieg um zehn Grad Celsius entspricht einem Anstieg um achtzehn Grad Fahrenheit. Measurement<UnitTemperature> aus Foundation bildet Temperaturmesswerte ab und leitet nicht aus einem Variablennamen ab, ob eine Differenz gemeint ist.

Das Datenmodell sollte deshalb ausdrücklich zwischen reading und delta unterscheiden; die Schnittstellendokumentation muss die jeweilige Einheit angeben. In der Geschäftslogik bleiben die Werte unformatiert. Die Anzahl der Nachkommastellen wird erst bei der Anzeige festgelegt. Oberflächentexte wie „dreiundzwanzig Komma fünf Grad“ dürfen nicht wieder als Eingabe für Berechnungen eingelesen werden.

Temperaturdifferenzen dürfen negativ sein; absolute Temperaturen unterhalb des absoluten Nullpunkts müssen dagegen abgelehnt werden. Für beide Felder darf nicht dieselbe Bereichsprüfung gelten.

Die folgende Validierung ist für physikalische Temperaturen gedacht. Verarbeitet das Produkt mathematisch extrapolierte Daten, muss dafür ein eigener Wertebereich definiert werden, statt die Eingabeprüfung stillschweigend zu lockern.

Ein Testpaket ohne Abhängigkeit von der Benutzeroberfläche anlegen

Prüfen Sie zunächst in einem separaten Verzeichnis die Toolchain und legen Sie dort das Paket an. So bleibt die Konfiguration des bestehenden Projekts unverändert. Die Befehle setzen installierte und funktionsfähige Xcode-Kommandozeilenwerkzeuge voraus; die Tests verwenden die aktuell ausgewählte Swift-Toolchain.

xcrun swift --version
workdir="$(mktemp -d)"
cd "$workdir"
swift package init --type library --name TemperatureChecks

Behalten Sie die erzeugten Dateien bei und speichern Sie den Code aus dem nächsten Abschnitt unter Tests/TemperatureChecksTests/TemperatureContractTests.swift. Er verwendet ausschließlich Foundation und XCTest; zusätzliche Pakete sind nicht erforderlich.

Das eigenständige Paket prüft die Umrechnungsregeln. Es bestätigt nicht, dass die Anzeige unter iOS bereits korrekt ist. Nach der Übernahme in das Projekt sollten die Tests die produktiv eingesetzte Implementierung aufrufen. Andernfalls könnten zwei Funktionskopien unabhängig voneinander geändert werden, während die Tests weiterhin nur die alte Kopie prüfen.

Regeln mit Referenzpunkten und Hin-und-zurück-Umrechnungen absichern

Bei absoluten Temperaturen wird zunächst geprüft, ob der Wert endlich ist. Anschließend erfolgt die Umrechnung in Kelvin, um die physikalische Untergrenze zu kontrollieren. Bei Temperaturdifferenzen wird der Nullpunktversatz entfernt, indem der umgerechnete Nullpunkt vom umgerechneten Endpunkt abgezogen wird. Dieses Vorgehen gilt hier nur für die lineare Temperaturumrechnung und darf nicht ungeprüft auf beliebige Einheitenmodelle übertragen werden.

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)
            )
        }
    }
}

Führen Sie im Stammverzeichnis des Pakets swift test aus. Die Toleranz dient dazu, Rundungsfehler bei Gleitkommaberechnungen abzufangen. Sie ist keine fachlich zulässige Messabweichung. Die Sensorgenauigkeit muss separat definiert werden; Fehler dürfen nicht durch eine großzügigere Toleranz in den Assertions verdeckt werden.

Auch eine erfolgreiche Hin-und-zurück-Umrechnung ersetzt keine Prüfung anhand von Referenzpunkten: Heben sich Fehler in beiden Umrechnungsrichtungen gegenseitig auf, kann der Test trotzdem bestehen. Behalten Sie mindestens vier Arten von Testfällen bei: Nullpunkt, negative Werte, bekannte Referenzwerte und ungültige Eingaben. Auch die Eingabeschnittstelle für Temperaturdifferenzen muss nicht endliche Werte ausdrücklich ablehnen. Die obige Hilfsfunktion zeigt nur die Umrechnung und übernimmt keine Eingabevalidierung.

Lokalisierung und numerische Genauigkeit getrennt prüfen

Speichern Sie den folgenden Code im Paketverzeichnis als format-temperature.swift und führen Sie ihn mit swift format-temperature.swift aus. Die Einheit ist hier fest auf Celsius eingestellt, damit regionale Präferenzen sie nicht automatisch ändern. So beschränkt sich die Prüfung auf die Darstellungsregeln.

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))
}

Prüfen Sie Dezimaltrennzeichen, Einheitenzeichen und Abstände. Gehen Sie jedoch nicht davon aus, dass die vollständige Zeichenfolge über verschiedene Betriebssystemversionen hinweg unverändert bleibt. Wenn Snapshot-Assertions benötigt werden, halten Sie auch die Betriebssystemversion und die Locale-Kennung fest.

Überlassen Sie die Darstellung von Temperaturdifferenzen nicht implizit einem Formatierer für absolute Temperaturen. Berechnen Sie zunächst die Differenz in der Zieleinheit und kennzeichnen Sie sie anschließend mit einem Zahlenformatierer und lokalisierten Ressourcen ausdrücklich als „Temperaturdifferenz“. Auch für negative Nullwerte braucht es eine Produktregel: Ob das Minuszeichen entfallen soll, wenn der gerundete Wert als null angezeigt wird, muss in der Darstellungsschicht einheitlich geregelt sein.

Bei der Integration drei Grenzen einhalten

Erstens müssen Rohdaten ihre Einheit mitführen. Eine unbekannte Einheit darf nicht stillschweigend als Celsius interpretiert werden, denn auch fehlerhafte Daten können dadurch in einem plausiblen Wertebereich liegen. Unbekannte Enum-Werte im Protokoll müssen zu einer eindeutigen Fehlermeldung führen.

Zweitens müssen Schwellenwertvergleiche mit ungerundeten Werten in einer einheitlichen Einheit erfolgen. Zwei Messwerte, die als dieselbe Zahl angezeigt werden, müssen nicht dieselbe Alarmentscheidung auslösen. Ein zusätzlicher Testfall mit unterschiedlichen Zahlenwerten, aber identischer Anzeige verhindert, dass eine Überarbeitung der Benutzeroberfläche die Geschäftslogik beeinflusst.

Drittens müssen Formatierung und Layout auch nach erfolgreichen Foundation-Tests auf dem Cloud-Mac noch in der vorgesehenen iOS-Laufzeitumgebung geprüft werden. Widgets, Achsenbeschriftungen in Diagrammen und Sprachausgabe sind jeweils separat abzunehmen. Eine Prüfung der Hauptansicht allein reicht nicht aus.

Bewahren Sie abschließend die Testergebnisse, die Toolchain-Version und die formatierten Ausgaben auf. Klären Sie bei einer Regression zunächst, ob sich die Umrechnung, der Eingabevertrag oder die Darstellung verändert hat, und korrigieren Sie dann die betreffende Schicht. Eine einzige Assertion für eine Zeichenfolge der Benutzeroberfläche kann nicht die gesamte Korrektheit absichern.

Häufig gestellte Fragen

Warum darf eine Temperaturdifferenz nicht wie ein absoluter Wert umgerechnet werden?

Die absolute Umrechnung enthält eine Verschiebung der Skalenursprünge. Rechnen Sie den Differenzwert und die Null der Ausgangseinheit um und subtrahieren Sie die Ergebnisse. Zehn Grad Celsius Differenz entsprechen achtzehn Grad Fahrenheit Differenz.

Sind exakte Zeichenkettenvergleiche für lokalisierte Temperaturen sinnvoll?

Nur bei festgelegter Systemversion, Locale, Einheit und Genauigkeit. Prüfen Sie sonst zunächst die Zahlenwerte und anschließend Rundung, Einheitenbeschriftung und Layout getrennt.

Den nächsten Build auf einem exklusiven Cloud-Mac ausführen

MacFTP bietet exklusive Apple-Silicon-Physikserver mit nativem macOS. Wählen Sie eine Laufzeit von einem Tag, einer Woche, einem Monat oder einem Quartal. Prüfen Sie zuerst die Konfiguration und binden Sie anschließend Ihre vorhandene Toolchain an den Server an. Den aktuellen Verfügbarkeitsstatus sehen Sie in Echtzeit im Portal.

Jetzt Cloud-Mac mieten

Vom Lesen zurück zum Projekt

Erst Konfiguration prüfen, dann Bereitstellung planen

Vergleichen Sie Modelle und Laufzeiten oder lesen Sie weitere technische Beiträge im Blog.