Media3 fournit des artefacts de test dédiés (media3-test-utils et media3-test-utils-robolectric) pour vous aider à tester la lecture de contenus multimédias, les transitions d'état du lecteur, les composants d'interface utilisateur et les sources multimédias personnalisées dans les tests JVM locaux et les tests d'instrumentation sur l'appareil.
Cette page décrit les pratiques recommandées pour tester les applications qui utilisent ExoPlayer et explique comment utiliser les utilitaires de test de Media3 dans chaque environnement de test.
Recommandations pour les tests
Lorsque vous écrivez des tests automatisés pour la lecture de contenus multimédias, suivez ces principes de base :
- Privilégiez les composants réels ou les fakes Media3 aux mocks : évitez de simuler
PlayerouExoPlayeravec des frameworks tels que Mockito.Playercomporte des invariants d'état et des contrats d'écouteurs que les simulations n'appliquent pas, ce qui peut fragiliser les tests ou masquer des bugs. Utilisez plutôt une instanceExoPlayerréelle,FakePlayerou des fakes tels queFakeMediaSourceetFakeDataSource. Réservez les simulations pour vérifier les appels de rappel personnalisés si nécessaire. - Attendre de manière déterministe au lieu de mettre en veille :
ExoPlayercharge et lit les contenus multimédias de manière asynchrone sur un thread interne. Évitez les appelsThread.sleeparbitraires, qui ralentissent les tests et les rendent instables. UtilisezTestPlayerRunHelperdans les tests Robolectric et suspendez les extensionsPlayerouPlayerFencedans les tests d'instrumentation. Ces outils d'aide attendent des conditions de lecteur explicites et échouent immédiatement si une erreur de lecture se produit. - Accéder au lecteur sur son thread d'application : les instances
ExoPlayeret les assistants d'attente doivent être appelés sur le threadLooperde l'application du lecteur (généralement le thread principal). Pour en savoir plus, consultez les exigences d'ExoPlayer concernant les threads.
Choisir l'environnement de test approprié
Les différentes couches de votre intégration multimédia bénéficient de différents environnements de test :
| Cible "test" | Environnement recommandé | Principaux utilitaires Media3 |
|---|---|---|
Composants d'UI (PlayerView, UI Compose), ViewModels ou rappels MediaSession |
Tests Robolectric locaux (src/test) ou tests d'UI instrumentés (src/androidTest) |
FakePlayer |
Intégration de ExoPlayer, transitions de playlist, analyses et logique MediaSource ou DataSource personnalisée |
Tests Robolectric locaux (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper et FakeMediaSource |
Décodage MediaCodec, rendu Surface, sortie audio et DRM matériels réels |
Tests d'instrumentation sur des appareils physiques (src/androidTest) |
ExoPlayer.Builder, Player.await* extensions, PlayerFence |
Dans la mesure du possible, exécutez les tests de lecture d'instrumentation sur des appareils physiques plutôt que sur des émulateurs, car les implémentations de la pile multimédia de l'émulateur varient. Pour en savoir plus sur la compatibilité de l'émulateur, consultez le guide sur les appareils compatibles.
Ajouter des dépendances de test
Ajoutez les artefacts de test Media3 au bloc dependencies du fichier build.gradle.kts ou build.gradle de votre module en utilisant la même version que vos autres modules Media3 :
Kotlin
testImplementation("androidx.media3:media3-test-utils:1.11.1")
testImplementation("androidx.media3:media3-test-utils-robolectric:1.11.1")
androidTestImplementation("androidx.media3:media3-test-utils:1.11.1")
Groovy
testImplementation "androidx.media3:media3-test-utils:1.11.1"
testImplementation "androidx.media3:media3-test-utils-robolectric:1.11.1"
androidTestImplementation "androidx.media3:media3-test-utils:1.11.1"
Les deux modules répartissent les responsabilités par environnement d'exécution :
media3-test-utils: utilitaires multi-environnements pour les tests locaux et les tests d'instrumentation, y comprisTestExoPlayerBuilder,PlayerFence(et les extensions KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestetMedia3FlagsRule.media3-test-utils-robolectric: utilitaires pour les tests Robolectric sur la JVM locale, y comprisTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryetPlaybackOutput.
Assurez-vous que votre module inclut également les dépendances du framework de test pour vos environnements de test :
- Robolectric :
org.robolectric:robolectric. Définissezandroid.testOptions.unitTests.isIncludeAndroidResources = truelors du chargement des éléments locaux. - AndroidX Test :
androidx.test:runner,androidx.test:coreetandroidx.test.ext:junit. - Coroutines Kotlin sur le thread principal :
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Activer les API de test instables
Les classes et les méthodes de media3-test-utils et media3-test-utils-robolectric sont annotées avec @UnstableApi. Bien que Media3 vise à maintenir l'ergonomie des outils d'aide aux tests largement utilisés d'une version à l'autre, les utilitaires de test ne suivent pas les mêmes règles de stabilité de compatibilité binaire que les API de lecture de production.
Dans vos ensembles de sources de test, annotez les classes ou méthodes de test avec @androidx.annotation.OptIn, ajoutez une activation package-info pour vos packages de test ou configurez lint.xml pour les sources de test. Pour en savoir plus, consultez les notes de dépannage du lint de l'API instable.
Tests locaux avec Robolectric
Les tests Robolectric locaux s'exécutent sur la JVM de votre ordinateur de développement, sans appareil physique ni émulateur. Robolectric ne fournit pas d'implémentations MediaCodec et maintient le Looper principal en pause jusqu'à ce qu'il soit explicitement pompé. Par conséquent, une instance ExoPlayer par défaut ne peut pas décoder de contenus multimédias ni fournir de mises à jour d'état par elle-même.
Créer un lecteur de test et avancer la lecture
Utilisez TestExoPlayerBuilder pour créer un ExoPlayer configuré pour les tests Robolectric. Par défaut, TestExoPlayerBuilder configure les composants suivants :
- Un
FakeClock(isAutoAdvancing = true) à progression automatique qui coordonne la progression temporelle entre le thread de lecture en arrière-plan d'ExoPlayeret leLooperprincipal de Robolectric en pause. - Faux moteurs de rendu vidéo et audio (
FakeVideoRendereretFakeAudioRenderer) qui consomment des échantillons de médias en fonction de leurs horodatages sans nécessiter de décodeursMediaCodec.
Pour faire progresser la lecture et vérifier les résultats sur le thread principal (avec un lecteur utilisant Looper.getMainLooper, la valeur par défaut dans Robolectric), utilisez les méthodes statiques advance(player) ou play(player) sur TestPlayerRunHelper (play(player) appelle player.play avant d'attendre). Dans la plupart des cas, l'assistant pompe le Looper principal de Robolectric jusqu'à ce que la condition cible soit remplie et génère une exception si une erreur de lecture fatale ou non fatale se produit :
Kotlin
@RunWith(AndroidJUnit4::class) @OptIn(UnstableApi::class) class PlaybackRobolectricTest { private lateinit var player: ExoPlayer @After fun tearDown() { if (::player.isInitialized) { player.release() } } @Test fun play_sampleMediaItem_reachesEndedState() { val context = ApplicationProvider.getApplicationContext<Context>() player = TestExoPlayerBuilder(context).build() player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")) player.prepare() play(player).untilState(Player.STATE_ENDED) assertThat(player.playbackState).isEqualTo(Player.STATE_ENDED) } }
Java
@RunWith(AndroidJUnit4.class) @OptIn(markerClass = UnstableApi.class) public static class PlaybackRobolectricTest { private @Nullable ExoPlayer player; @After public void tearDown() { if (player != null) { player.release(); } } @Test public void play_sampleMediaItem_reachesEndedState() throws Exception { Context context = ApplicationProvider.getApplicationContext(); player = new TestExoPlayerBuilder(context).build(); player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); player.prepare(); play(player).untilState(Player.STATE_ENDED); assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED); } }
Dans cet exemple, asset:///media/mp4/sample.mp4 fait référence à un exemple de fichier multimédia placé dans le répertoire src/test/assets/ (ou src/main/assets/) de votre module.
Lorsqu'une référence ExoPlayer est transmise, advance(player) et play(player) renvoient un ExoPlayerRunResult (ou PlayerRunResult lorsqu'un Player est transmis). Ces types de résultats fournissent des méthodes pour attendre les états de lecture, les positions, les transitions de playlist, les images affichées, les commandes en attente et les erreurs de lecture attendues, ou pour ignorer les erreurs non fatales. Pour en savoir plus sur les conditions d'attente, consultez la documentation de référence de l'API TestPlayerRunHelper.
Tester avec de fausses sources et des codecs simulés
De nombreux tests d'intégration vérifient la navigation dans la playlist, la sélection des titres, les mises à jour de la chronologie ou le comportement de nouvelle tentative, et n'ont pas besoin de fichiers multimédias réels. Vous pouvez insérer des chronologies et des données synthétiques dans un lecteur TestExoPlayerBuilder à l'aide de FakeMediaSource, FakeTimeline, FakeDataSource ou WebServerDispatcher (avec OkHttp MockWebServer) :
Kotlin
val player = TestExoPlayerBuilder(context).build() val fakeMediaSource = FakeMediaSource(FakeTimeline(/* windowCount= */ 2)) player.setMediaSource(fakeMediaSource) player.prepare() play(player).untilMediaItemIndex(/* mediaItemIndex= */ 1) assertThat(player.currentMediaItemIndex).isEqualTo(1) player.release()
Java
ExoPlayer player = new TestExoPlayerBuilder(context).build(); FakeMediaSource fakeMediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2)); player.setMediaSource(fakeMediaSource); player.prepare(); play(player).untilMediaItemIndex(/* mediaItemIndex= */ 1); assertThat(player.getCurrentMediaItemIndex()).isEqualTo(1); player.release();
TestExoPlayerBuilder utilise des moteurs de rendu fictifs par défaut. Si votre test Robolectric nécessite des interactions MediaCodec, configurez les codecs simulés et la capture de sortie comme suit :
- Ajoutez
withAllDefaultSupportedCodecsdeShadowMediaCodecConfigen tant que@RuleJUnit pour enregistrer les codecs simulés de Robolectric. - Transmettez un
CapturingRenderersFactorycréé avec leClock(CapturingRenderersFactory(context, builder.getClock())) du compilateur àbuilder.setRenderersFactory. - Lorsque vous testez des pistes vidéo, associez un
Surface(tel queSurface(SurfaceTexture(1))) àplayer.setVideoSurfaceafin queMediaCodecVideoRendereraffiche les images décodées au lieu de les ignorer. - Pour vérifier le résultat affiché, appelez
register(player, capturingRenderersFactory)surPlaybackOutputavant de lancer la lecture. - Une fois la lecture terminée, libérez le lecteur et la surface, puis inspectez la sortie formatée de
Dumper(Dumper().add(playbackOutput).toString()).
Tests d'instrumentation sur des appareils
Les tests Robolectric locaux n'exécutent pas de décodeurs de plate-forme, de pistes audio, de sessions DRM ni de rendu Surface réels. Pour vérifier le comportement de lecture de bout en bout sur les appareils équipés d'Android, écrivez des tests d'instrumentation dans src/androidTest à l'aide d'un ExoPlayer réel créé avec ExoPlayer.Builder.
Dans les tests d'instrumentation JUnit, les méthodes @Test s'exécutent sur un thread d'instrumentation plutôt que sur le thread principal de l'application. Créez, contrôlez et libérez toujours ExoPlayer sur son thread d'application Looper, par exemple en utilisant runBlocking(Dispatchers.Main) en Kotlin ou InstrumentationRegistry.getInstrumentation().runOnMainSync en Java.
Attendre les états du lecteur avec PlayerFence et les coroutines
Dans les tests d'instrumentation, le Looper principal s'exécute en temps réel sur l'appareil.
N'incluez pas Robolectric ni media3-test-utils-robolectric dans les dépendances androidTestImplementation. Pour attendre les états et les événements de lecture, media3-test-utils fournit les outils d'aide suivants :
- Kotlin : suspension des fonctions d'extension sur
Player(telles queawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedetawaitContentPositionAtLeast). - Java : méthode statique
futureWhen(player)surPlayerFence, qui renvoie unPlayerFencedont les méthodes de condition (telles queentersPlaybackStateetisPlaying) doivent être appelées sur le threadLooperdu lecteur et renvoyer unListenableFutureque le thread de test peut bloquer avecget.
Les deux API supervisent le lecteur pendant l'attente. Si une erreur de lecture fatale (ou, par défaut, une erreur non fatale) se produit, ou si l'attente dépasse le délai avant expiration (10 secondes par défaut ou 30 secondes sur un émulateur), l'appel échoue immédiatement au lieu de se bloquer :
Kotlin
@RunWith(AndroidJUnit4::class) @OptIn(UnstableApi::class) class PlaybackInstrumentationTest { private lateinit var player: ExoPlayer @After fun tearDown() = runBlocking(Dispatchers.Main) { if (::player.isInitialized) { player.release() } } @Test fun play_sampleMediaItem_advancesAndEnds() = runBlocking(Dispatchers.Main) { val context = ApplicationProvider.getApplicationContext<Context>() player = ExoPlayer.Builder(context).build() player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")) player.prepare() player.play() player.awaitIsPlaying(true) player.awaitContentPositionAtLeast(500) player.awaitPlaybackState(Player.STATE_ENDED) assertThat(player.playbackState).isEqualTo(Player.STATE_ENDED) } }
Java
@RunWith(AndroidJUnit4.class) @OptIn(markerClass = UnstableApi.class) public static class PlaybackInstrumentationTest { private @Nullable ExoPlayer player; @After public void tearDown() { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { if (player != null) { player.release(); } }); } @Test public void play_sampleMediaItem_advancesAndEnds() throws Exception { SettableFuture<Void> playbackStarted = SettableFuture.create(); SettableFuture<Void> positionPassed = SettableFuture.create(); SettableFuture<Void> playbackEnded = SettableFuture.create(); InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { Context context = ApplicationProvider.getApplicationContext(); player = new ExoPlayer.Builder(context).build(); player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4")); playbackStarted.setFuture(futureWhen(player).isPlaying(true)); positionPassed.setFuture(futureWhen(player).passesContentPosition(500)); playbackEnded.setFuture(futureWhen(player).entersPlaybackState(Player.STATE_ENDED)); player.prepare(); player.play(); }); playbackStarted.get(); positionPassed.get(); playbackEnded.get(); InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> assertThat(player.getPlaybackState()).isEqualTo(Player.STATE_ENDED)); } }
Lorsque vous créez ExoPlayer avec ApplicationProvider.getApplicationContext dans un module d'application (com.android.application), les URI asset:/// sont résolus par rapport aux éléments de l'application cible (tels que src/debug/assets/ ou src/main/assets/, plutôt que src/androidTest/assets/).
Événements temporaires et supervision personnalisée
En plus de vérifier l'état actuel du lecteur (où les attentes sont renvoyées immédiatement si la condition est déjà remplie), les assistants de media3-test-utils acceptent les événements ponctuels et les conditions d'écoute personnalisées :
- Événements temporaires tels que la première image affichée : associez une sortie vidéo (comme
SurfaceouPlayerView) au lecteur, puis commencez à attendre avecawaitFirstFrameRenderedourendersFirstFrameavant d'appelerplayer.preparepour ne pas manquer un événement d'image ponctuel précoce. - Conditions personnalisées : encapsulez les écouteurs personnalisés ou les attentes de coroutine dans
awaitSupervisedsurplayeren Kotlin, ou transmettez unListenableFuturepersonnalisé àfutureWhen(player).withSupervision(future)en Java. Lorsque vous testez la récupération après des erreurs non fatales, transmettezfailOnNonFatalErrors = false(Kotlin) ou appelezignoringNonFatalErrors(Java), puis personnalisez les délais avant expiration avectimeoutouwithTimeoutMs(timeoutMs).
L'exemple suivant attend un événement transitoire de première image et supervise un écouteur de métadonnées personnalisé :
Kotlin
// Start awaiting transient events (with a video surface attached) before prepare(). coroutineScope { val firstFrameDeferred = async(start = CoroutineStart.UNDISPATCHED) { player.awaitFirstFrameRendered() } player.setMediaItem(mediaItem) player.prepare() player.play() firstFrameDeferred.await() } // Supervise a custom listener condition while still failing fast on player errors. val metadataTitle = player.awaitSupervised { player.mediaMetadata.title?.let { return@awaitSupervised it } val titleDeferred = CompletableDeferred<CharSequence>() val listener = object : Player.Listener { override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { mediaMetadata.title?.let { titleDeferred.complete(it) } } } player.addListener(listener) try { titleDeferred.await() } finally { player.removeListener(listener) } }
Java
SettableFuture<Void> firstFrameFuture = SettableFuture.create(); SettableFuture<CharSequence> metadataTitleFuture = SettableFuture.create(); SettableFuture<CharSequence> supervisedTitleFuture = SettableFuture.create(); Player.Listener metadataListener = new Player.Listener() { @Override public void onMediaMetadataChanged(MediaMetadata mediaMetadata) { if (mediaMetadata.title != null) { metadataTitleFuture.set(mediaMetadata.title); } } }; try { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { // Register transient event fences (with a video surface attached) before prepare(). firstFrameFuture.setFuture(futureWhen(player).rendersFirstFrame()); // Supervise a custom ListenableFuture so player errors fail fast. player.addListener(metadataListener); supervisedTitleFuture.setFuture( futureWhen(player).withSupervision(metadataTitleFuture)); player.setMediaItem(mediaItem); if (player.getMediaMetadata().title != null) { metadataTitleFuture.set(player.getMediaMetadata().title); } player.prepare(); player.play(); }); firstFrameFuture.get(); CharSequence title = supervisedTitleFuture.get(); } finally { InstrumentationRegistry.getInstrumentation() .runOnMainSync(() -> { player.removeListener(metadataListener); firstFrameFuture.cancel(/* mayInterruptIfRunning= */ true); supervisedTitleFuture.cancel(/* mayInterruptIfRunning= */ true); }); }
Tester l'UI et les composants de l'application avec FakePlayer
Lorsque vous testez des composants d'interface utilisateur tels que des composables PlayerView ou media3-ui-compose, des ViewModels ou des rappels MediaSession, il n'est souvent pas nécessaire de créer un ExoPlayer complet. FakePlayer dans media3-test-utils étend SimpleBasePlayer pour simuler des playlists, des états de lecture, des positions, des tailles de vidéo, des repères et des erreurs sur le Looper du thread d'appel. Ce Looper est disponible automatiquement dans les tests Robolectric avec @RunWith(AndroidJUnit4::class) et sur le thread principal dans les tests d'instrumentation.
Par défaut, FakePlayer simule un délai de mise en mémoire tampon de 100 ms sur prepare et seekTo. Transmettez bufferingDelayMs = 0 en Kotlin ou définissez les états directement avec setPlaybackState :
Kotlin
val fakePlayer = FakePlayer(bufferingDelayMs = 0) fakePlayer.setMediaItem(MediaItem.fromUri("https://example.com/media.mp4")) fakePlayer.prepare() fakePlayer.play() assertThat(fakePlayer.isPlaying).isTrue() // Drive state transitions directly to test UI or session listeners. fakePlayer.setPosition(30_000) fakePlayer.setPlaybackState(Player.STATE_ENDED) fakePlayer.release()
Java
FakePlayer fakePlayer = new FakePlayer(); fakePlayer.setMediaItem(MediaItem.fromUri("https://example.com/media.mp4")); // Drive state transitions directly (prepare() simulates a 100 ms buffering delay by default). fakePlayer.setPlaybackState(Player.STATE_READY); fakePlayer.play(); assertThat(fakePlayer.isPlaying()).isTrue(); fakePlayer.setPosition(30_000); fakePlayer.setPlaybackState(Player.STATE_ENDED); fakePlayer.release();
Utilitaires de test supplémentaires
media3-test-utils inclut des assistants spécialisés pour d'autres scénarios de test :
- Flags de fonctionnalité et mode Canary : utilisez
Media3FlagsRuleavec@BindFlaget@BindCanaryModepour réinitialiser les remplacements de flags après chaque test et exécuter des tests paramétrés sur les états des flags. Pour en savoir plus, consultez Tester avec des flags de fonctionnalité. - Tests de contrat
DataSourcepersonnalisés : sous-classezDataSourceContractTestpour vérifier que les implémentationsDataSourceouHttpDataSourcepersonnalisées sont conformes aux attentes du contrat de Media3 concernant les requêtes de plage, la gestion des exceptions et les écouteurs de transfert. Pour en savoir plus, consultez Intégrer d'autres piles réseau.