iOS-Gleitkommawerte auf dem Cloud-Mac an ihren Grenzen testen

iOS-Gleitkommawerte auf dem Cloud-Mac an ihren Grenzen testen

iOS-Gleitkommawerte auf dem Cloud-Mac an ihren Grenzen testen

Wenn sich ein Fortschrittsbalken gelegentlich nicht aktualisiert oder ein Diagramm plötzlich leer bleibt, liegt das Problem nicht unbedingt am UI-Thread. Möglicherweise wurde bereits zuvor ein NaN-Wert weitergereicht. Sowohl das Teilen eines Sensorwerts durch den Messbereichsendwert als auch das Teilen der erledigten Menge durch die Gesamtmenge kann ungültige Ergebnisse liefern. Auf einem Cloud-Mac von ArmShell lässt sich diese Berechnung zunächst in ein eigenständiges Swift-Paket auslagern, mit Grenzwerttests gegen fehlerhafte Werte absichern und anschließend in das iOS-Projekt integrieren.

Erst den Zahlenvertrag festlegen, dann die Toleranz wählen

Dieser Artikel behandelt genau einen fachlichen Anwendungsfall: einen nicht negativen Messwert auf den Bereich von null bis eins zu normieren. Die Eingaben müssen endlich sein, der Messbereichsendwert muss größer als null sein, und das Ergebnis muss endlich sein und innerhalb des zulässigen Bereichs liegen. Ungültige Daten führen zu einem Fehler und werden nicht automatisch auf die Grenzen begrenzt. Ein Messwert von null ist zulässig; eine negative Null wird immer als positive Null ausgegeben.

Eine Bedingung wird dabei leicht übersehen: value < 0 || value > 1 schließt NaN nicht aus. Geordnete Vergleiche mit NaN liefern immer false. Das Programm könnte den Wert deshalb weiterhin als gültiges Ergebnis behandeln.

Endlichkeit, fachlicher Wertebereich und näherungsweise Gleichheit sind drei unterschiedliche Prüfebenen. Eine größere Testtoleranz behebt keine ungültigen Eingaben.

Auch die Fehlerbehandlung sollte von Anfang an feststehen: Die Oberfläche kann „Daten nicht verfügbar“ anzeigen, darf einen Fehler aber nicht stillschweigend in null umwandeln. Sonst können Nutzer einen echten Nullwert nicht von einem fehlgeschlagenen Lesevorgang unterscheiden.

Ein Testpaket ohne Simulator-Abhängigkeit erstellen

Prüfen Sie zunächst, ob die Kommandozeilenwerkzeuge verfügbar sind, und erstellen Sie das Paket anschließend in einem temporären Verzeichnis. Die folgenden Schritte verändern das App-Projekt nicht und erfordern keine grafische Sitzung:

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

Ersetzen Sie den Inhalt von Sources/NumericBoundary/NumericBoundary.swift durch:

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
}

Nur die Eingaben zu prüfen, reicht nicht aus. Auch die Division zweier endlicher Zahlen kann einen unendlichen Wert ergeben, etwa wenn ein sehr großer Zähler durch einen sehr kleinen positiven Nenner geteilt wird. Ebenso muss ein endliches Ergebnis außerhalb des Messbereichs als Fehler behandelt werden. Die Berechnungsschicht darf sich nicht darauf verlassen, dass UI-Komponenten solche Fälle abfangen.

Diese Implementierung lässt die üblichen Gleitkommarundungen zu. Falls fachlich zusätzlich ein kleinster verwertbarer Messwert erforderlich ist, muss dafür ein eigener Schwellenwert definiert werden. Bei der Division sehr kleiner positiver Zahlen kann ein Unterlauf zu null führen; das wird in diesem Beispiel akzeptiert. „Endlich“ darf nicht mit „vollständig ohne Genauigkeitsverlust“ gleichgesetzt werden.

Fehlerpfade mit Gegenbeispielen abdecken

Ersetzen Sie den Inhalt der standardmäßig angelegten Testdatei durch den folgenden Code. Jeder Fall prüft die Fehlerkategorie, damit der Test nicht fälschlich erfolgreich ist, wenn die Funktion aus einem anderen Grund einen Fehler wirft:

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

Bei einem gewöhnlichen Gleichheitsvergleich ist negative Null gleich positiver Null. Deshalb muss zusätzlich sign geprüft werden, um nachzuweisen, dass die Normierungsregel tatsächlich greift. Verwenden Sie für Fehlerfälle auch keine Assertion nach dem Muster „Das Ergebnis ist ungleich NaN“: NaN ist nicht einmal mit sich selbst gleich. Ein solcher Test hat daher keine Aussagekraft.

Toleranzen an die fachliche Größenordnung binden

Der Ausgabebereich dieses Beispiels ist fest auf null bis eins begrenzt. Ein Vergleich anhand des absoluten Fehlers ist deshalb naheliegend. 1e-12 ist eine Testentscheidung für diese kleine Berechnung, kein allgemeiner Genauigkeitsstandard für Sensorik, Grafik oder Statistik.

Wenn die berechneten Werte viele Größenordnungen abdecken, prüfen Sie zunächst, ob beide Werte endlich sind. Kombinieren Sie anschließend je nach fachlicher Anforderung absolute und relative Fehlertoleranz:

abs(actual - expected)
    <= max(absoluteTolerance,
           relativeTolerance * max(abs(actual), abs(expected)))

Auch beide Toleranzen selbst müssen endlich und nicht negativ sein. Bei extremen Größenordnungen können die Differenz oder die Multiplikation auf der rechten Seite überlaufen. Die Formel allein ist daher noch keine allgemein sichere Bibliothekslösung.

In der Nähe von null ist vor allem die absolute Toleranz maßgeblich, bei großen Zahlen vor allem die relative. Im Testcode sollte stehen, woher die Schwellenwerte stammen, etwa aus der Geräteauflösung oder dem Fehlerbudget des Algorithmus. Sie sollten nicht so lange erhöht werden, bis der Test besteht. Formatieren Sie die Werte auch nicht vorab als Dezimalzeichenfolgen, um diese anschließend zu vergleichen: Das würde tatsächliche Unterschiede verdecken.

In die Pipeline integrieren und die Wirksamkeit der Tests nachweisen

Führen Sie im Paketverzeichnis Folgendes aus:

swift test
swift test -c release

Die Tests müssen sowohl in der Debug- als auch in der Release-Konfiguration laufen. Für die Prüfungen im Produktivcode werden guard und das Werfen von Fehlern verwendet, nicht assert, das bei optimierten Builds entfernt werden kann. Die Pipeline muss den Exit-Code der Befehle beibehalten und bei fehlgeschlagenen Tests alle weiteren Auslieferungsschritte stoppen.

Prüfen Sie anschließend durch einen bewusst eingebauten Fehler, ob die Absicherung greift: Entfernen Sie vorübergehend raw.isFinite aus der Eingabeprüfung und vergewissern Sie sich, dass der NaN-Testfall wegen der geänderten Fehlerkategorie fehlschlägt. Stellen Sie den Code danach wieder her. So weisen Sie nach, dass die Tests tatsächlich die Eingabeprüfung abdecken und nicht bloß die Funktion ausgeführt haben.

Bei der Integration in die App muss der Fachcode dieselbe Implementierung aufrufen. Pflegen Sie keine separate Kopie im Testpaket. Es genügt, die Toolchain-Version, die Commit-Kennung und die fehlgeschlagenen Testfälle zu protokollieren; echte Messwerte von Nutzern müssen nicht hochgeladen werden. Die abschließenden Abnahmekriterien lauten: Ungültige Eingaben führen eindeutig zu einem Fehler, gültige Grenzwerte bestehen zuverlässig, die Toleranzen sind begründet, und derselbe Vertrag wird auch in der Release-Konfiguration durchgesetzt.

Häufig gestellte Fragen

Erkennt eine Prüfung auf Werte unter null oder über eins auch NaN?

Nein. Beide Vergleiche ergeben bei NaN false. Prüfen Sie zuerst isFinite und danach den zulässigen Bereich.

Muss das Ergebnis bei endlichen Eingaben erneut geprüft werden?

Ja. Auch die Division endlicher Werte kann Unendlich ergeben. Prüfen Sie deshalb Endlichkeit und Wertebereich des Ergebnisses.

Bereit für Ihren nächsten Build mit einem Cloud-Mac

Wählen Sie Modell und Laufzeit passend zu Ihrer Arbeitslast. Dedizierte physische Apple-Silicon-Knoten statt virtueller Maschinen.

Cloud-Mac mieten

Vom Lesen zum ersten Build

Stellen Sie Ihre Arbeitslast zusammen und wählen Sie den passenden dedizierten physischen Knoten.