Media3 fornisce artefatti di test dedicati (media3-test-utils e
media3-test-utils-robolectric) per aiutarti a testare la riproduzione di contenuti multimediali, le transizioni di stato del player, i componenti della UI e le origini multimediali personalizzate sia nei test JVM locali sia nei test di instrumentazione dei dispositivi.
In questa pagina vengono descritte le pratiche consigliate per testare le app che utilizzano ExoPlayer
e viene mostrato come utilizzare le utilità di test di Media3 in ogni ambiente di test.
Suggerimenti per i test
Quando scrivi test automatici per la riproduzione di contenuti multimediali, segui questi principi fondamentali:
- Preferisci i componenti reali o i falsi di Media3 ai mock: evita di simulare
PlayeroExoPlayercon framework come Mockito.Playerha invarianti di stato e contratti di listener che i mock non applicano, il che può rendere i test fragili o nascondere bug. Utilizza invece un'istanzaExoPlayerreale,FakePlayero falsi comeFakeMediaSourceeFakeDataSource. Riserva i mock per verificare le chiamate di callback personalizzate quando necessario. - Attendi in modo deterministico anziché sospendere:
ExoPlayeresegue il caricamento e la riproduzione dei contenuti multimediali in modo asincrono su un thread interno. Evita chiamateThread.sleeparbitrarie, che rendono i test lenti e instabili. UtilizzaTestPlayerRunHelpernei test Robolectric e sospendi le estensioniPlayeroPlayerFencenei test di instrumentazione. Questi helper attendono condizioni esplicite del player e generano immediatamente un errore se si verifica un errore di riproduzione. - Accedi al player nel relativo thread dell'applicazione: le istanze
ExoPlayere gli helper in attesa devono essere chiamati nel thread dell'applicazioneLooperdel player (in genere il thread principale). Per saperne di più, consulta i requisiti di threading di ExoPlayer.
Scegli l'ambiente di test giusto
I diversi livelli dell'integrazione dei contenuti multimediali traggono vantaggio da diversi ambienti di test:
| Target di test | Ambiente consigliato | Utilità Media3 principali |
|---|---|---|
Componenti UI (PlayerView, Compose UI), ViewModels o callback MediaSession |
Test Robolectric locali (src/test) o test UI con strumentazione (src/androidTest) |
FakePlayer |
Integrazione di ExoPlayer, transizioni delle playlist, analisi e logica MediaSource o DataSource personalizzata |
Test Robolectric locali (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource |
Decodifica MediaCodec, rendering Surface, uscita audio e DRM hardware reali |
Test di instrumentazione su dispositivi fisici (src/androidTest) |
ExoPlayer.Builder, Player.await* estensioni, PlayerFence |
Se possibile, esegui i test di riproduzione della strumentazione su dispositivi fisici anziché su emulatori, perché le implementazioni dello stack multimediale dell'emulatore variano. Per saperne di più sul supporto dell'emulatore, consulta la guida ai dispositivi supportati.
Aggiungi dipendenze di test
Aggiungi gli artefatti di test di Media3 al blocco dependencies del file build.gradle.kts o build.gradle del tuo modulo utilizzando la stessa versione degli altri moduli 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")
Alla moda
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"
I due moduli dividono le responsabilità in base all'ambiente di runtime:
media3-test-utils: utilità cross-environment per test locali e test di instrumentazione, tra cuiTestExoPlayerBuilder,PlayerFence(ed estensioni KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTesteMedia3FlagsRule.media3-test-utils-robolectric: utilità per i test Robolectric sulla JVM locale, tra cuiTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryePlaybackOutput.
Assicurati che il modulo includa anche le dipendenze del framework di test per i tuoi ambienti di test:
- Robolectric:
org.robolectric:robolectric. Impostaandroid.testOptions.unitTests.isIncludeAndroidResources = truequando carichi gli asset locali. - AndroidX Test:
androidx.test:runner,androidx.test:coreeandroidx.test.ext:junit. - Coroutine Kotlin sul thread principale:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Attiva le API di test instabili
Le classi e i metodi in media3-test-utils e media3-test-utils-robolectric
sono annotati con @UnstableApi. Sebbene Media3 miri a mantenere ergonomici gli helper di test ampiamente utilizzati tra le release, le utilità di test non seguono le stesse regole di stabilità della compatibilità binaria delle API di riproduzione di produzione.
Nei set di risorse di test, annota le classi o i metodi di test con
@androidx.annotation.OptIn, aggiungi un'attivazione package-info per i pacchetti di test o configura lint.xml per le origini di test. Per saperne di più, consulta le note sulla risoluzione dei problemi relativi al lint dell'API instabile.
Test locali con Robolectric
I test Robolectric locali vengono eseguiti sulla JVM della macchina di sviluppo senza un dispositivo fisico o un emulatore. Robolectric non fornisce
implementazioni di MediaCodec e mantiene il Looper principale in pausa finché non viene
esplicitamente attivato, quindi un'istanza ExoPlayer predefinita non può decodificare i contenuti multimediali o
fornire aggiornamenti di stato autonomamente.
Crea un player di test e avanza la riproduzione
Utilizza TestExoPlayerBuilder per creare un ExoPlayer configurato per i test Robolectric. Per impostazione predefinita, TestExoPlayerBuilder configura i seguenti componenti:
- Un
FakeClock(isAutoAdvancing = true) con avanzamento automatico che coordina la progressione temporale tra il thread di riproduzione in background diExoPlayere ilLooperprincipale in pausa di Robolectric. - Renderer video e audio fittizi (
FakeVideoRenderereFakeAudioRenderer) che utilizzano campioni multimediali in base ai relativi timestamp senza richiedere decodificatoriMediaCodec.
Per far avanzare la riproduzione e verificare i risultati sul thread principale (con un player che utilizza
Looper.getMainLooper, l'impostazione predefinita in Robolectric), utilizza i metodi statici
advance(player) o play(player) su
TestPlayerRunHelper (play(player) chiama
player.play prima di attendere). Per la maggior parte delle condizioni, l'helper esegue il loop principale di Robolectric Looper finché non viene soddisfatta la condizione target e genera un'eccezione se si verifica un errore di riproduzione irreversibile o non irreversibile:
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); } }
In questo esempio, asset:///media/mp4/sample.mp4 si riferisce a un file multimediale di esempio inserito nella directory src/test/assets/ (o src/main/assets/) del modulo.
Quando viene passato un riferimento ExoPlayer, advance(player) e play(player)
restituiscono un ExoPlayerRunResult (o
PlayerRunResult quando viene passato un Player). Questi tipi di risultati
forniscono metodi per attendere stati di riproduzione, posizioni, transizioni
della playlist, frame visualizzati, comandi in attesa ed errori di riproduzione previsti o
per ignorare errori non irreversibili. Per ulteriori informazioni sulle condizioni di attesa, consulta il riferimento API TestPlayerRunHelper.
Esegui test con origini fittizie e codec simulati
Molti test di integrazione verificano la navigazione delle playlist, la selezione delle tracce, gli aggiornamenti della sequenza temporale o il comportamento dei nuovi tentativi e non richiedono file multimediali reali. Puoi inserire
timeline e dati sintetici in un player TestExoPlayerBuilder utilizzando
FakeMediaSource, FakeTimeline,
FakeDataSource o
WebServerDispatcher (con 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 utilizza renderer fittizi per impostazione predefinita. Se il test Robolectric richiede invece interazioni MediaCodec, configura i codec simulati e l'acquisizione dell'output come segue:
- Aggiungi
withAllDefaultSupportedCodecsdaShadowMediaCodecConfigcome@RuleJUnit per registrare i codec simulati di Robolectric. - Passa un
CapturingRenderersFactorycreato conClock(CapturingRenderersFactory(context, builder.getClock())) del builder abuilder.setRenderersFactory. - Quando testi le tracce video, collega un
Surface(ad esempioSurface(SurfaceTexture(1))) conplayer.setVideoSurfacein modo cheMediaCodecVideoRendereresegua il rendering dei frame decodificati anziché saltarli. - Per verificare l'output sottoposto a rendering, chiama
register(player, capturingRenderersFactory)suPlaybackOutputprima di avviare la riproduzione. - Al termine della riproduzione, rilascia il player e la superficie, quindi esamina l'output formattato da
Dumper(Dumper().add(playbackOutput).toString()).
Test di instrumentazione sui dispositivi
I test Robolectric locali non eseguono decodificatori di piattaforme, tracce audio, sessioni DRM o rendering Surface reali. Per verificare il comportamento di riproduzione end-to-end sui dispositivi con Android, scrivi test di instrumentazione in src/androidTest utilizzando un ExoPlayer reale creato con ExoPlayer.Builder.
Nei test di instrumentazione JUnit, i metodi @Test vengono eseguiti su un thread di instrumentazione anziché sul thread principale dell'applicazione. Crea, controlla e rilascia sempre ExoPlayer nel thread Looper dell'applicazione, ad esempio utilizzando runBlocking(Dispatchers.Main) in Kotlin o InstrumentationRegistry.getInstrumentation().runOnMainSync in Java.
Attendi gli stati del player con PlayerFence e le coroutine
Nei test di instrumentazione, il Looper principale viene eseguito in tempo reale sul dispositivo.
Non includere Robolectric o media3-test-utils-robolectric nelle dipendenze di androidTestImplementation. Per attendere gli stati e gli eventi di riproduzione, media3-test-utils fornisce i seguenti helper:
- Kotlin: funzioni di estensione di sospensione su
Player(comeawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedeawaitContentPositionAtLeast). - Java: il metodo statico
futureWhen(player)suPlayerFence, che restituisce unPlayerFencei cui metodi di condizione (comeentersPlaybackStateeisPlaying) devono essere chiamati sul threadLooperdel player e restituiscono unListenableFuturesu cui il thread di test può bloccarsi conget.
Entrambe le API supervisionano il player durante l'attesa. Se si verifica un errore di riproduzione irreversibile (o, per impostazione predefinita, un errore non irreversibile) o se l'attesa supera il timeout (10 secondi per impostazione predefinita o 30 secondi su un emulatore), la chiamata non va a buon fine immediatamente anziché bloccarsi:
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)); } }
Quando crei ExoPlayer con ApplicationProvider.getApplicationContext in un modulo dell'app (com.android.application), gli URI asset:/// vengono risolti in base agli asset dell'app di destinazione (ad esempio src/debug/assets/ o src/main/assets/, anziché src/androidTest/assets/).
Eventi temporanei e supervisione personalizzata
Oltre a controllare lo stato attuale del player (dove le attese vengono restituite immediatamente se la condizione è già soddisfatta), gli helper in media3-test-utils supportano eventi una tantum e condizioni di listener personalizzate:
- Eventi temporanei come il primo frame visualizzato: collega un output video (ad esempio
SurfaceoPlayerView) al player e inizia ad attendere conawaitFirstFrameRenderedorendersFirstFrameprima di chiamareplayer.prepareper non perdere un evento frame iniziale una tantum. - Condizioni personalizzate: inserisci i listener personalizzati o le attese delle coroutine all'interno di
awaitSupervisedsuplayerin Kotlin o passa unListenableFuturepersonalizzato afutureWhen(player).withSupervision(future)in Java. Quando testi il ripristino da errori non irreversibili, passafailOnNonFatalErrors = false(Kotlin) o chiamaignoringNonFatalErrors(Java) e personalizza i timeout contimeoutowithTimeoutMs(timeoutMs).
L'esempio seguente attende un evento first-frame temporaneo e supervisiona un listener di metadati personalizzato:
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); }); }
Testa la UI e i componenti dell'app con FakePlayer
Quando si testano componenti UI come i componibili PlayerView o media3-ui-compose, i ViewModel o i callback MediaSession, spesso non è necessario creare un ExoPlayer completo. FakePlayer in
media3-test-utils estende SimpleBasePlayer per
simulare playlist, stati di riproduzione, posizioni, dimensioni dei video, cue ed errori sul
Looper del thread chiamante. Questo Looper è disponibile automaticamente nei test Robolectric con @RunWith(AndroidJUnit4::class) e nel thread principale nei test di instrumentazione.
Per impostazione predefinita, FakePlayer simula un ritardo di buffering di 100 ms su prepare e seekTo; passa bufferingDelayMs = 0 in Kotlin o imposta gli stati direttamente con 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();
Utilità di test aggiuntive
media3-test-utils include helper specializzati per altri scenari di test:
- Flag di funzionalità e modalità canary: utilizza
Media3FlagsRuleinsieme a@BindFlage@BindCanaryModeper reimpostare gli override dei flag dopo ogni test ed eseguire test parametrizzati tra gli stati dei flag. Per saperne di più, consulta Test con i flag di funzionalità. - Test dei contratti
DataSourcepersonalizzati: sottoclasseDataSourceContractTestper verificare che le implementazioni personalizzate diDataSourceoHttpDataSourcesiano conformi alle aspettative del contratto di Media3 per le richieste di intervalli, la gestione degli errori e i listener di trasferimento. Per saperne di più, consulta Integrazione di altri stack di rete.