Tester le debounce de recherche iOS avec une horloge virtuelle

Tester le debounce de recherche iOS avec une horloge virtuelle

Tester le debounce de recherche iOS avec une horloge virtuelle

Le champ de recherche applique un debounce de 300 millisecondes, mais le test se contente d’« attendre une demi-seconde après la saisie, puis de vérifier le nombre de requêtes ». Dans des tâches exécutées en parallèle sur un Mac dans le cloud, ce type de test est à la fois lent et sensible aux retards d’ordonnancement. Ce qu’il faut réellement valider, c’est l’échéance calculée après la dernière saisie, le remplacement de la saisie précédente et l’absence de livraison après l’effacement du champ. En séparant ces règles du minuteur, les tests n’ont plus besoin d’attendre que le temps réel s’écoule.

Définir d’abord le contrat temporel de la recherche

Cet article utilise un debounce sur le front descendant : chaque saisie non vide recalcule l’échéance et, une fois celle-ci atteinte, seul le dernier terme de recherche est transmis, une seule fois. Les espaces en début et en fin de saisie sont d’abord supprimés ; une chaîne composée uniquement de caractères blancs, comme une chaîne vide, annule le terme en attente. Saisir de nouveau le même terme relance également le délai. Il s’agit d’une règle produit explicite, et non du comportement par défaut de toutes les implémentations.

Le temps est représenté par un nombre entier de millisecondes écoulées depuis le début du test, et non par une date système. En production, le temps écoulé doit provenir d’une horloge monotone, pas de l’horloge civile, qui peut subir des sauts lors d’une remise à l’heure. Les 300 millisecondes ne sont qu’un exemple de politique de debounce, pas un indicateur de performance du service.

Le debounce détermine uniquement quand transmettre le terme de recherche. L’annulation éventuelle d’une requête réseau déjà envoyée et l’autorisation donnée à une ancienne réponse de mettre à jour l’interface relèvent de deux autres contrats. Constater que « le minuteur a été annulé » ne suffit pas à les valider.

Isoler les règles temporelles dans une machine à états

Créez un petit paquet de validation depuis la ligne de commande du Mac dans le cloud. La chaîne d’outils de développement actuellement sélectionnée doit inclure Swift ; aucun simulateur ni aucune session graphique n’est nécessaire ici.

mkdir SearchTiming
cd SearchTiming
swift package init --type library --name SearchTiming

Remplacez le contenu de Sources/SearchTiming/SearchTiming.swift par le code suivant. Il ne crée ni thread ni minuteur : l’appelant doit fournir des valeurs de temps dans un ordre non décroissant et accéder à l’état de manière sérialisée.

import Foundation

struct SearchDebounce {
    let delay: Int64
    private(set) var deadline: Int64?
    private var pending: String?

    init(delay: Int64 = 300) {
        precondition(delay > 0)
        self.delay = delay
    }

    mutating func input(_ text: String, at now: Int64) {
        let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !query.isEmpty else {
            pending = nil
            deadline = nil
            return
        }
        pending = query
        deadline = now + delay
    }

    mutating func takeDue(at now: Int64) -> String? {
        guard let limit = deadline, now >= limit else {
            return nil
        }
        let query = pending
        pending = nil
        deadline = nil
        return query
    }
}

takeDue vérifie l’échéance et consomme le terme en attente dans la même opération. Des rappels répétés ne peuvent donc pas provoquer plusieurs transmissions, et un rappel trop précoce ne supprime pas le terme en attente. Cet exemple suppose que les valeurs temporelles restent dans une plage normale de fonctionnement. Si l’adaptateur d’horloge accepte des valeurs externes, il faut aussi vérifier les valeurs négatives, les retours en arrière et les débordements lors de l’addition.

Faire avancer le temps virtuel sans appeler sleep

Remplacez le contenu de Tests/SearchTimingTests/SearchTimingTests.swift par les tests ci-dessous. Chaque cas crée son propre état afin de ne partager ni horloge ni saisie en attente.

import XCTest
@testable import SearchTiming

final class SearchTimingTests: XCTestCase {
    func testDeadlineAndSingleDelivery() {
        var state = SearchDebounce()
        state.input(" swift ", at: 0)
        XCTAssertNil(state.takeDue(at: 299))
        XCTAssertEqual(state.takeDue(at: 300), "swift")
        XCTAssertNil(state.takeDue(at: 301))
    }

    func testLatestInputMovesDeadline() {
        var state = SearchDebounce()
        state.input("s", at: 0)
        state.input("sw", at: 100)
        state.input("swift", at: 220)
        XCTAssertEqual(state.deadline, 520)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertNil(state.takeDue(at: 519))
        XCTAssertEqual(state.takeDue(at: 520), "swift")
    }

    func testBlankCancelsPendingInput() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input(" \n ", at: 100)
        XCTAssertNil(state.deadline)
        XCTAssertNil(state.takeDue(at: 1000))
    }

    func testSameInputRestartsDelay() {
        var state = SearchDebounce()
        state.input("swift", at: 0)
        state.input("swift", at: 200)
        XCTAssertNil(state.takeDue(at: 300))
        XCTAssertEqual(state.takeDue(at: 500), "swift")
    }
}

Exécutez swift test. L’échec d’une assertion doit obligatoirement produire un code de sortie non nul ; le pipeline ne doit pas se contenter d’archiver la sortie puis de poursuivre. Ces tests couvrent l’instant juste avant l’échéance, l’échéance exacte et la consommation répétée. Ajoutez un cas nettement postérieur à l’échéance pour vérifier qu’un retard d’ordonnancement ne fait pas perdre définitivement le terme en attente.

Éviter deux erreurs d’annulation lors du raccordement aux tâches réelles

Pour l’intégration réelle, vous pouvez utiliser ContinuousClock afin d’attendre l’échéance. La machine à états porte les règles ; l’adaptateur assure le réveil. À chaque saisie, mettez d’abord l’état à jour, puis annulez l’ancienne tâche d’attente et créez-en une pour la nouvelle échéance. Lorsque le champ est vidé, aucune nouvelle tâche ne doit être créée.

La première erreur consiste à masquer l’erreur d’annulation. Si la logique de transmission est appelée directement après une attente avec try?, le chemin d’annulation peut continuer son exécution. Il faut quitter ce chemin lorsque l’annulation est interceptée, et non traiter celle-ci comme si l’échéance avait été atteinte.

La seconde erreur est le réveil tardif d’une ancienne tâche. Associez un numéro de génération à chaque saisie ; la tâche d’attente conserve le numéro correspondant à sa création. À son réveil, elle doit comparer ce numéro à la génération actuelle sur le même exécuteur série, et ne peut consommer l’état que si les deux correspondent. Vérifier uniquement si la tâche a été annulée ne suffit pas, car l’annulation et le réveil peuvent survenir simultanément.

La saisie dans l’interface, la mise à jour de la génération et la transmission du résultat peuvent toutes être prises en charge par l’exécuteur principal. Ne lisez pas l’état en arrière-plan avant de revenir de manière asynchrone sur l’exécuteur principal pour le modifier : cela laisserait un intervalle entre la vérification et la consommation. Dans les tests virtuels, les millisecondes servent seulement à simplifier les assertions ; pour l’attente réelle, utilisez les types d’échéance et de durée de l’horloge afin d’éviter l’accumulation d’erreurs de conversion.

Terminer par quelques tests d’intégration ciblés

Une fois les tests de logique pure réussis, vérifiez l’adaptateur réel au lieu de réintroduire de longues attentes dans tous les cas de test. Prévoyez un délai maximal de test suffisamment large, tout en vérifiant qu’aucune transmission n’a lieu trop tôt et qu’une seule transmission finit par avoir lieu. Ce délai maximal sert uniquement à détecter un blocage, pas à établir une assertion temporelle précise.

La liste de validation doit au minimum couvrir les points suivants : une succession rapide de saisies ne transmet que la dernière ; vider le champ annule immédiatement le terme en attente ; une erreur d’annulation ne déclenche pas la transmission ; les rappels d’une ancienne génération sont ignorés ; l’interface n’est plus mise à jour après sa libération ; plusieurs exécutions successives produisent le même résultat. Si le produit exige la déduplication des termes de recherche identiques, ajoutez une couche distincte et précisez si un terme peut être recherché à nouveau après avoir vidé le champ puis l’avoir ressaisi.

Il suffit de conserver la révision du code, la version de la chaîne d’outils et le code de sortie des tests ; il n’est pas nécessaire d’enregistrer les recherches réelles des utilisateurs. Le résultat n’est plus un script qui « devrait passer après une demi-seconde d’attente », mais un ensemble d’assertions capable d’indiquer exactement quelle règle temporelle a été enfreinte.

Questions fréquentes

Le temps virtuel remplace-t-il tous les tests de minuteur réel ?

Non. Il valide les règles de la machine à états. Quelques tests d’intégration doivent vérifier l’horloge monotone, l’annulation et la remise du résultat sur l’exécuteur prévu.

Vider le champ annule-t-il une requête déjà envoyée ?

Non. Cette machine annule seulement le texte en attente d’envoi. Annulez séparément les requêtes actives et filtrez les réponses obsolètes avec une génération de requête.

Un Mac dédié pour vos projets

Des nœuds Mac mini physiques, pas des machines virtuelles. Louez à la journée, à la semaine, au mois ou au trimestre. Choisissez votre usage et votre configuration, puis vérifiez la disponibilité en temps réel dans la console.

Choisir un modèle et une durée Voir tous les tarifs