Die Summe der einzelnen Bestellpositionen weicht um einen Cent vom Gesamtbetrag des Servers ab, und bei Erstattungen entstehen an negativen Halbwerten weitere Abweichungen: Solche Fehler lassen sich nicht anhand von Screenshots abnehmen. Kapseln Sie das Parsen von Geldbeträgen und die Rundungsregeln in reinen Funktionen, unabhängig von der Oberfläche. Führen Sie auf einem Cloud-Mac Tests mit festen Testfällen aus, um zunächst nachzuweisen, dass Client und Server denselben Berechnungsvertrag einhalten. Erst danach binden Sie die Regeln in die Bestellseite ein. Dieser Artikel behandelt ausschließlich numerische Regeln. Es wird weder ein Zahlungs-Gateway angebunden noch mit echten Transaktionsdaten gearbeitet.
Zuerst den Berechnungsvertrag für Geldbeträge festlegen
Vor den Tests müssen vier Punkte feststehen: Wie viele Nachkommastellen sind bei der Eingabe zulässig? Wie viele bleiben für die Abrechnung erhalten? An welcher Stelle wird gerundet? Und in welche Richtung wird gerundet, wenn ein Wert genau auf der Mitte liegt? Im folgenden Beispiel sind höchstens sechs Nachkommastellen bei der Eingabe und zwei bei der Abrechnung vorgesehen. Exakte Halbwerte werden von null weg gerundet. Das ist ein Beispielvertrag, keine für alle Geschäftsprozesse geeignete Vorgabe.
Rabattsätze, Stückpreise und Abrechnungsbeträge sind unterschiedliche Größen. Nach der Multiplikation von Stückpreis und Menge darf die Zwischenpräzision erhalten bleiben; der endgültige Betrag muss jedoch durch die vereinbarte Abrechnungsfunktion laufen. Überlassen Sie es nicht den einzelnen Aufrufern, Werte zu formatieren und anschließend wieder in Zahlen umzuwandeln.
Decimal löst das Problem der Dezimaldarstellung, legt aber keine Geschäftsregeln fest. Sowohl das Runden jeder einzelnen Position als auch das Runden der Gesamtsumme kann sinnvoll sein. Innerhalb einer Bestellung dürfen diese Verfahren jedoch nicht vermischt werden.
Klären Sie zuerst, ob der Server Dezimalzeichenfolgen oder ganzzahlige Werte in der kleinsten Währungseinheit zurückgibt. Liefert er tatsächlich Gleitkommazahlen, muss zunächst der Schnittstellenvertrag angepasst werden. Der Client kann den ursprünglichen Wert nicht aus bereits verlorener Präzision rekonstruieren.
Ein unabhängig ausführbares Regelpaket erstellen
Erstellen Sie in einem leeren Verzeichnis ein Swift-Bibliothekspaket. Schreiben Sie den Regelcode in die erzeugte Quelldatei und die Tests in das zugehörige Testverzeichnis. Führen Sie die Befehle im Projektverzeichnis auf dem Cloud-Mac aus. Ein Simulator muss dafür nicht gestartet werden.
mkdir MoneyRules
cd MoneyRules
swift package init --type library --name MoneyRules
swift --version
Ersetzen Sie Sources/MoneyRules/MoneyRules.swift durch die folgende Implementierung. Sie verwendet eine feste Locale zum Parsen, begrenzt die Eingabelänge und weist Tausendertrennzeichen, Exponentialschreibweise, Whitespace sowie mehr Nachkommastellen als vereinbart zurück. Hier geht es um Schnittstellendaten, nicht um lokalisierte Benutzereingaben.
import Foundation
public enum MoneyRules {
public static func parse(_ text: String) -> Decimal? {
guard text.utf8.count <= 20,
text.range(
of: #"^-?[0-9]+(?:\.[0-9]{1,6})?\z"#,
options: .regularExpression
) != nil else {
return nil
}
return Decimal(
string: text,
locale: Locale(identifier: "en_US_POSIX")
)
}
public static func settle(_ value: Decimal) -> Decimal {
var input = value
var output = Decimal()
NSDecimalRound(&output, &input, 2, .plain)
return output
}
}
Verwenden Sie nicht Decimal(Double(text)!). Auch wenn am Ende ein Decimal entsteht, hat die zwischenzeitliche Umwandlung in eine binäre Gleitkommazahl die numerische Grundlage bereits verändert. Die maximale Eingabelänge muss ebenfalls zum tatsächlichen Wertebereich des Geschäftsprozesses passen. Beträge dürfen nicht willkürlich gekürzt werden, nur damit Tests bestehen.
Unterschiede an Halbwerten und in der Berechnungsreihenfolge aufdecken
Schreiben Sie den folgenden Inhalt in Tests/MoneyRulesTests/MoneyRulesTests.swift und ersetzen Sie damit die Standardtests. Vergleichen Sie Decimal-Werte exakt, ohne einen Toleranzbereich für „zulässige Abweichungen“. Andernfalls könnte der Test selbst eine Differenz von einem Cent verdecken.
import XCTest
@testable import MoneyRules
final class MoneyRulesTests: XCTestCase {
private func number(_ text: String) throws -> Decimal {
try XCTUnwrap(MoneyRules.parse(text))
}
func testMidpoints() throws {
XCTAssertEqual(
MoneyRules.settle(try number("1.005")),
try number("1.01")
)
XCTAssertEqual(
MoneyRules.settle(try number("-1.005")),
try number("-1.01")
)
XCTAssertEqual(
MoneyRules.settle(try number("1.004")),
try number("1.00")
)
}
func testRoundingStage() throws {
let item = try number("0.335")
let perLine = MoneyRules.settle(item) * Decimal(3)
let total = MoneyRules.settle(item * Decimal(3))
XCTAssertEqual(perLine, try number("1.02"))
XCTAssertEqual(total, try number("1.01"))
}
func testRejectedInputs() {
for text in ["", "1,25", "1e2", "NaN", " 1.00",
"1.0000001", "1.00\n"] {
XCTAssertNil(MoneyRules.parse(text))
}
}
}
Führen Sie swift test aus und prüfen Sie, dass alle drei Tests erfolgreich sind. Der Test zur Berechnungsreihenfolge empfiehlt kein bestimmtes Abrechnungsverfahren. Er zeigt lediglich, dass beide Verfahren tatsächlich unterschiedliche Ergebnisse liefern. Sobald eines davon für den Geschäftsprozess festgelegt wurde, sollten zusätzliche Tests auf Bestellebene verhindern, dass die Aufrufkette auf das andere Verfahren ausweicht.
Ergänzen Sie weitere Testfälle für null, negative null, Erstattungen und den maximal zulässigen Betrag sowie drei Gruppen mit Werten unterhalb, genau auf und oberhalb der Rundungsmitte. Wenn Sie stattdessen Halbwerte zur geraden Zahl runden wollen, müssen Sie die entsprechenden Erwartungswerte neu festlegen, statt nur die Implementierung zu ändern.
Übertragung und Anzeige getrennt prüfen
Die Datenübertragung über die Schnittstelle darf nicht von einem Formatierer für die Benutzeroberfläche abhängen. Für die Anzeige können Gruppierungszeichen hinzugefügt oder Dezimaltrennzeichen an die jeweilige Region angepasst werden. Übertragen wird weiterhin die vereinbarte Dezimalzeichenfolge. Das folgende Beispiel prüft, ob sich das Abrechnungsergebnis nach der Umwandlung in eine Zeichenfolge exakt wieder einlesen lässt. Es garantiert nicht, dass die Zeichenfolge immer zwei Nachkommastellen enthält.
import Foundation
var settled = MoneyRules.settle(MoneyRules.parse("12.345")!)
let wire = NSDecimalString(
&settled,
Locale(identifier: "en_US_POSIX")
)
precondition(MoneyRules.parse(wire) == settled)
Übernehmen Sie diese Prüfung in einen Test, statt das Produktionsprogramm bei fehlerhaften Daten unmittelbar zu beenden. Verlangt das Protokoll ein festes Format wie "12.30", müssen Sie dafür einen separaten Encoder implementieren und testen. Numerische Gleichheit allein reicht nicht aus, um die Zeichenfolge abzunehmen.
Speichern Sie für gemeinsame Testfälle von Client und Server die Eingabezeichenfolge, die Menge, den Rundungsschritt und die erwartete Zeichenfolge, aber keine echten Bestellnummern. Nur wenn Server und Client denselben Satz von Testfällen unabhängig einlesen, lässt sich bestätigen, dass beide dieselben Regeln anwenden.
In die Pipeline einbinden und überprüfbare Nachweise sichern
Führen Sie zuerst die Tests des eigenständigen Pakets und danach die Anwendungstests aus. Schlagen die Pakettests fehl, stoppen Sie die nachfolgenden Schritte zur Paketerstellung. So gelangen Fehler in den numerischen Regeln nicht erst zur Abnahme der Benutzeroberfläche. Halten Sie die Commit-Kennung, die Swift-Version und fehlgeschlagene Testfälle fest. Laufzeitangaben sind kein Ersatz für einen Nachweis der Korrektheit.
Halten Sie die Prüfliste kurz und eindeutig:
- Die Eingaben stammen aus Dezimalzeichenfolgen und wurden nicht über Double umgewandelt.
- Gerundet wird nur im vereinbarten Schritt; Zwischenergebnisse werden nicht wiederholt auf weniger Nachkommastellen begrenzt.
- Negative Halbwerte und Erstattungsregeln sind durch eigene Assertions abgesichert.
- Übertragene Werte lassen sich exakt wieder einlesen; Anzeigetexte fließen nicht in die Berechnung ein.
- Beide Seiten verwenden dieselben Testfälle. Bei Regeländerungen werden Erwartungswerte und die Beschreibung der Geschäftsregeln gemeinsam aktualisiert.
Erfolgreiche reine Foundation-Tests belegen lediglich, dass die Berechnungsschicht den aktuellen Vertrag erfüllt. Abschließend müssen Sie weiterhin im iOS-Test-Target die Eingabe und Anzeige auf der Seite prüfen und dabei entdeckte Cent-Abweichungen als feste Testfälle ergänzen. Ein erneut ausführbarer Test für jede Korrektur ist verlässlicher als die Erinnerung an einen einmaligen manuellen Abgleich.
Häufig gestellte Fragen
Verhindert Decimal automatisch falsche Geldbeträge?
Nein. Der Umweg über Double, zu frühes Runden oder unterschiedliche Regeln können weiterhin Abweichungen erzeugen. Verwenden Sie geprüfte Dezimalzeichenketten und legen Sie den Rundungszeitpunkt fest.
Ist dafür ein iOS-Simulator erforderlich?
Reine Foundation-Berechnungen lassen sich im Swift-Paket mit swift test ausführen. Eingaben in der Oberfläche, lokalisierte Anzeigen und iOS-spezifischer Code benötigen separate Anwendungstests.
Build-Warteschlange an einen exklusiven Cloud-Mac übergeben
Wählen Sie einen physischen Knoten, ein Modell und einen Abrechnungszeitraum. So laufen Xcode-Builds und Automatisierungsaufgaben in Ihrer eigenen macOS-Umgebung. Der aktuelle Verfügbarkeitsstatus wird in Echtzeit in der Konsole angezeigt.