Tester les calculs monétaires iOS avec Decimal sur un Mac cloud

Tester les calculs monétaires iOS avec Decimal sur un Mac cloud

Tester les calculs monétaires iOS avec Decimal sur un Mac cloud

La somme des lignes d’une commande diffère d’un centime du total calculé côté serveur, et le montant d’un remboursement présente lui aussi un écart lorsqu’une valeur négative tombe exactement à mi-chemin entre deux centimes : ce type de défaut ne se valide pas à partir de captures d’écran. Isolez l’analyse des montants et les règles d’arrondi dans des fonctions pures, puis exécutez un jeu de données fixe sur un Mac cloud. Vérifiez d’abord que les deux côtés respectent le même contrat de calcul, avant d’intégrer ces fonctions à la page de commande. Cet article traite uniquement des règles numériques : il ne se connecte à aucune passerelle de paiement et n’utilise aucune donnée de transaction réelle.

Définir d’abord le contrat de calcul des montants

Avant les tests, fixez quatre points : le nombre de décimales autorisé en entrée, le nombre de décimales conservé pour le montant final, l’étape à laquelle appliquer l’arrondi et le sens de l’arrondi lorsque la valeur se trouve exactement à mi-chemin. L’exemple ci-dessous accepte jusqu’à six décimales en entrée, en conserve deux pour le montant final et arrondit les valeurs à mi-chemin en s’éloignant de zéro. Ce contrat sert d’exemple ; il ne convient pas à tous les besoins métier.

Un taux de remise, un prix unitaire et un montant final ne sont pas des grandeurs interchangeables. Le produit du prix unitaire par la quantité peut conserver une précision intermédiaire, mais le montant final doit passer par la fonction d’arrondi convenue. Ne laissez pas chaque appelant formater le résultat à sa manière, puis le reconvertir en valeur numérique.

Decimal résout le problème de la représentation décimale, mais ne définit pas vos règles métier. Arrondir chaque ligne ou arrondir après avoir calculé le total peut être pertinent dans les deux cas, mais ces méthodes ne doivent pas être mélangées au sein d’une même commande.

Vérifiez d’abord si le serveur renvoie une chaîne décimale ou un entier exprimé dans la plus petite unité monétaire. S’il renvoie en réalité un nombre à virgule flottante, il faut commencer par modifier le contrat de l’API : le client ne peut pas retrouver la valeur d’origine une fois la précision perdue.

Créer un paquet de règles exécutable de façon autonome

Créez un paquet de bibliothèque Swift dans un répertoire vide. Placez les règles dans le fichier source généré et les tests dans le répertoire de tests correspondant. Exécutez ces commandes dans le répertoire du projet sur le Mac cloud ; il n’est pas nécessaire de démarrer un simulateur.

mkdir MoneyRules
cd MoneyRules
swift package init --type library --name MoneyRules
swift --version

Remplacez le contenu de Sources/MoneyRules/MoneyRules.swift par l’implémentation suivante. Elle fixe les paramètres régionaux utilisés pour l’analyse, limite la longueur de l’entrée et rejette les séparateurs de milliers, la notation exponentielle, les espaces et les valeurs dont le nombre de décimales dépasse la limite convenue. Elle traite les données de l’API, pas les saisies utilisateur localisées.

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

N’écrivez pas Decimal(Double(text)!). Même si le résultat final est un Decimal, la conversion intermédiaire en virgule flottante binaire a déjà altéré la valeur issue du texte. La limite de longueur doit, elle aussi, correspondre à la plage réellement autorisée par le métier ; il ne faut pas tronquer arbitrairement un montant pour faire passer les tests.

Détecter les écarts aux points médians et selon l’ordre de calcul

Placez le contenu suivant dans Tests/MoneyRulesTests/MoneyRulesTests.swift, à la place des tests par défaut. Comparez les valeurs Decimal de façon exacte, sans marge de tolérance : une tolérance pourrait masquer précisément l’écart d’un centime que le test doit détecter.

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

Exécutez swift test et vérifiez que les trois tests réussissent. Le test sur l’ordre de calcul ne recommande pas une méthode plutôt que l’autre : il montre qu’elles produisent effectivement des résultats différents. Une fois la méthode choisie selon les besoins métier, ajoutez des tests à l’échelle de la commande pour empêcher qu’un autre maillon de la chaîne d’appels utilise la méthode opposée.

Complétez ensuite les cas de test avec zéro, zéro négatif, les remboursements, le montant maximal autorisé et trois groupes de valeurs : en dessous du point médian, exactement au point médian et au-dessus. Si vous adoptez l’arrondi au pair en cas d’égalité, redéfinissez les résultats attendus correspondants au lieu de modifier uniquement l’implémentation.

Valider séparément la transmission et l’affichage

Les données transmises par l’API ne doivent pas dépendre du formateur utilisé par l’interface. L’affichage peut ajouter des séparateurs de groupement ou adapter le séparateur décimal aux paramètres régionaux ; la transmission doit toujours utiliser la chaîne décimale convenue. L’exemple suivant vérifie qu’un montant arrondi, converti en chaîne puis relu, conserve exactement sa valeur. Il ne garantit pas que la chaîne comporte systématiquement deux décimales.

import Foundation

var settled = MoneyRules.settle(MoneyRules.parse("12.345")!)
let wire = NSDecimalString(
    &settled,
    Locale(identifier: "en_US_POSIX")
)
precondition(MoneyRules.parse(wire) == settled)

Intégrez cette vérification aux tests plutôt que de laisser le programme de production s’arrêter brutalement lorsqu’il rencontre des données incorrectes. Si le protocole exige un format fixe tel que "12.30", implémentez et testez un encodeur distinct : l’égalité numérique ne suffit pas à valider la chaîne.

Dans les jeux de données partagés entre client et serveur, conservez la chaîne d’entrée, la quantité, l’étape d’arrondi et la chaîne attendue, mais aucun véritable numéro de commande. Le serveur et le client doivent lire le même jeu de données pour vérifier qu’ils appliquent bien les mêmes règles.

Intégrer les tests au pipeline et conserver des preuves vérifiables

Exécutez d’abord les tests du paquet autonome, puis ceux de l’application. Si les tests du paquet échouent, interrompez les étapes suivantes de génération des livrables pour éviter qu’une erreur de calcul ne soit découverte seulement lors de la validation de l’interface. Consignez l’identifiant du commit, la version de Swift et les cas en échec ; une durée d’exécution ne remplace pas une conclusion sur l’exactitude des résultats.

Gardez une liste de vérifications courte et explicite :

La réussite de tests utilisant uniquement Foundation prouve seulement que la couche de calcul respecte le contrat actuel. Il reste à vérifier la saisie et l’affichage de la page dans une cible de tests iOS, puis à ajouter au jeu de données fixe tout écart au centime constaté. Conserver un test réexécutable pour chaque correction est plus fiable que de se fier au souvenir d’une vérification manuelle.

Questions fréquentes

Decimal garantit-il des calculs monétaires exacts ?

Non. Une conversion préalable par Double, un arrondi prématuré ou des règles différentes peuvent encore créer des écarts. Partez de chaînes décimales validées et définissez l’étape d’arrondi.

Faut-il lancer un simulateur iOS pour ces tests ?

Les calculs purs utilisant Foundation se testent avec swift test dans un paquet Swift. La saisie dans l’interface, l’affichage localisé et le code propre à iOS nécessitent des tests applicatifs distincts.

Confiez vos builds à un Mac cloud dédié

Choisissez un nœud physique, un modèle et une durée de facturation pour exécuter vos builds Xcode et vos tâches d’automatisation dans votre propre environnement macOS. L’état de disponibilité est indiqué en temps réel dans la console.

Poursuivez la vérification de votre environnement de build

Comparez les caractéristiques des modèles et les instructions pour définir la configuration à suivre.