Les tests automatisés d’un lecteur audio peuvent tous réussir sur un Mac dans le cloud, puis laisser passer un problème bien réel : l’utilisateur débranche son casque et la lecture continue sur le haut-parleur. Le décodeur n’est pas forcément en cause. C’est peut-être la décision à prendre après un changement de sortie qui n’a jamais été testée séparément. Il en va de même pour les interruptions audio du système : leur fin ne signifie pas que l’utilisateur souhaite reprendre la lecture. En séparant les notifications système des décisions de lecture, on peut vérifier cette logique de façon fiable sur un nœud de build dépourvu de casque.
Définir les événements et les comportements attendus
Définissez d’abord les règles du produit, puis écrivez le code qui traite les notifications. Cet article adopte une règle prudente : une interruption survenant pendant la lecture met celle-ci en pause et mémorise qu’elle était en cours. La lecture ne reprend à la fin de l’interruption que si la notification autorise cette reprise. Si l’utilisateur appuie lui-même sur pause pendant l’interruption, l’intention de reprendre doit être annulée. La déconnexion d’un périphérique de sortie met directement la lecture en pause : le nom de la nouvelle sortie ne suffit pas à déduire que l’utilisateur accepte de continuer sur le haut-parleur.
| Événement reçu | État de lecture | Intention de reprise |
|---|---|---|
| Début d’une interruption pendant la lecture | En pause | Mémoriser que la lecture était en cours |
| Fin de l’interruption avec reprise autorisée | Dépend de l’intention de reprise | Effacer |
| Mise en pause par l’utilisateur | En pause | Effacer |
| Ancien périphérique de sortie devenu indisponible | En pause | Effacer |
« Ancien périphérique de sortie devenu indisponible » désigne une cause précise, et non tous les changements de sortie. Par exemple, le passage à un autre périphérique déjà connecté ne doit pas être traité comme un casque débranché sans décision explicite concernant le comportement du produit.
Isoler les décisions de lecture dans une machine à états pure
Laissez l’analyse des notifications dans une couche d’adaptation : la logique centrale ne reçoit que des événements explicites. Les types ci-dessous peuvent être placés dans la cible de l’application et importés par la cible de test. isPlaying représente l’état de lecture souhaité par l’application ; c’est à la couche externe d’appeler le lecteur lorsque cet état change.
enum AudioEvent {
case interruptionBegan
case interruptionEnded(shouldResume: Bool)
case outputDisconnected
case userPaused
}
struct PlaybackState {
var isPlaying = false
var resumeAfterInterruption = false
mutating func apply(_ event: AudioEvent) {
switch event {
case .interruptionBegan:
resumeAfterInterruption = isPlaying
isPlaying = false
case .interruptionEnded(let shouldResume):
isPlaying = resumeAfterInterruption && shouldResume
resumeAfterInterruption = false
case .outputDisconnected, .userPaused:
isPlaying = false
resumeAfterInterruption = false
}
}
}
Cet exemple ne couvre qu’une seule interruption. Si l’application gère plusieurs sessions de lecture simultanément, ou une file d’attente qui reste utilisable après une pause, enrichissez d’abord la définition de l’état, puis déterminez l’ordre de traitement des événements. Un seul booléen ne doit pas porter toute la sémantique des sessions.
Traduire les notifications système en événements
La couche d’adaptation écoute AVAudioSession.interruptionNotification et lit le type d’interruption dans userInfo. À la fin de l’interruption, elle lit l’option de reprise et transmet le résultat à la machine à états. Pour AVAudioSession.routeChangeNotification, elle vérifie la cause du changement et ne traduit que oldDeviceUnavailable en outputDisconnected. Les champs absents ou inconnus doivent être consignés pour investigation, jamais interprétés d’office comme une autorisation de reprise.
Les callbacks de notification et les actions de l’interface peuvent provenir de contextes d’exécution différents. Transmettez les événements en série à un même propriétaire de l’état, puis commandez la pause ou la lecture selon l’ancien et le nouvel état. Ainsi, lorsque « pause demandée par l’utilisateur » et « fin de l’interruption » surviennent en concurrence, l’ordre d’arrivée des callbacks ne risque pas d’écraser accidentellement l’intention de l’utilisateur.
Exécuter les tests des cas limites sur un nœud de build
Commencez par couvrir avec XCTest deux cas faciles à oublier : une interruption ne doit pas démarrer un lecteur jusque-là inactif ; une pause demandée par l’utilisateur pendant l’interruption doit empêcher la reprise automatique. Ces tests n’ont besoin ni d’un vrai casque ni de notifications système émises à un moment précis.
import XCTest
final class PlaybackStateTests: XCTestCase {
func testInterruptionDoesNotStartIdlePlayer() {
var state = PlaybackState()
state.apply(.interruptionBegan)
state.apply(.interruptionEnded(shouldResume: true))
XCTAssertFalse(state.isPlaying)
}
func testUserPauseCancelsResume() {
var state = PlaybackState(isPlaying: true)
state.apply(.interruptionBegan)
state.apply(.userPaused)
state.apply(.interruptionEnded(shouldResume: true))
XCTAssertFalse(state.isPlaying)
}
func testDisconnectedOutputStopsPlayback() {
var state = PlaybackState(isPlaying: true)
state.apply(.outputDisconnected)
XCTAssertFalse(state.isPlaying)
XCTAssertFalse(state.resumeAfterInterruption)
}
}
Sur le Mac dans le cloud, vérifiez d’abord le projet et les Schemes avec xcodebuild -list -project App.xcodeproj, puis lancez les tests avec le Scheme réellement partagé par l’équipe : xcodebuild test -project App.xcodeproj -scheme App -destination 'platform=iOS Simulator,name=<已安装的设备名称>'. Remplacez le nom du projet, le Scheme et le nom du périphérique utilisés dans cet exemple ; xcrun simctl list devices available permet de voir les périphériques actuellement disponibles. La cible de test doit pouvoir accéder aux types de la machine à états, et le Scheme doit inclure cette cible.
La réussite des tests unitaires montre seulement que les séquences d’événements fournies produisent les décisions attendues. Elle ne prouve ni que les notifications système seront reçues, ni que le son sortira effectivement du périphérique attendu.
Compléter la validation sur un appareil réel
Prévoyez au minimum quatre vérifications sur appareil : débrancher le casque pendant la lecture et confirmer que le haut-parleur ne prend pas le relais de façon inattendue ; provoquer une interruption audio système maîtrisée et observer les notifications de début et de fin ; mettre manuellement en pause pendant l’interruption et confirmer que la lecture reste en pause ensuite ; reconnecter un périphérique de sortie et vérifier que cette seule connexion ne déclenche pas la lecture. Il suffit de consigner les types d’événements reçus, leurs causes et l’état final de lecture. N’inscrivez dans les journaux ni contenu utilisateur ni identifiants d’accès.
Si les tests unitaires réussissent mais qu’un problème persiste sur l’appareil, vérifiez d’abord que l’écoute des notifications reste active pendant toute la durée de vie de la session. Examinez ensuite l’analyse des champs, l’ordre de transmission des événements et la synchronisation entre l’état réel du lecteur et PlaybackState. Distinguez en particulier « la machine à états demande une pause » de « le lecteur s’est effectivement mis en pause » : les tests garantissent le premier point ; le second exige une vérification de la couche d’adaptation et une validation sur appareil.
Conserver ces vérifications dans le processus de fusion
Avant la fusion, confirmez que les tests de la machine à états couvrent l’inactivité, l’interruption, la pause demandée par l’utilisateur et la déconnexion d’un périphérique ; que la couche d’adaptation traite prudemment les champs inconnus ; que tous les événements suivent le même chemin d’exécution sérialisé ; et que l’équipe a consigné au moins un résultat de validation sur l’appareil cible. Un Mac dans le cloud convient aux tests d’état déterministes exécutés à répétition, tandis que le routage audio réel doit toujours être vérifié sur appareil. En gardant ces deux niveaux de validation distincts, on sait, en cas d’échec, s’il faut examiner la règle métier, la traduction des notifications ou la sortie audio elle-même.
Questions fréquentes
Un test unitaire sur simulateur suffit-il pour valider le retrait d'un casque ?
Non. Il valide la décision après un événement injecté ; la notification reçue et la sortie réelle doivent aussi être vérifiées sur un appareil.
Faut-il toujours reprendre la lecture après une interruption ?
Non. Vérifiez la lecture avant l'interruption, l'autorisation de reprise et toute pause demandée entre-temps par l'utilisateur.
Lancez votre prochaine compilation Xcode sur un Mac physique dédié.
Choisissez votre modèle Apple Silicon, votre région et la durée de location, puis exécutez votre propre processus de compilation à distance. Les disponibilités sont celles indiquées dans la console au moment de la commande.