Media3 proporciona artefactos de prueba dedicados (media3-test-utils y media3-test-utils-robolectric) para ayudarte a probar la reproducción de contenido multimedia, las transiciones de estado del reproductor, los componentes de la IU y las fuentes de contenido multimedia personalizadas en las pruebas de JVM locales y las pruebas de instrumentación de dispositivos.
En esta página, se describen las prácticas recomendadas para probar apps que usan ExoPlayer
y se muestra cómo usar las utilidades de prueba de Media3 en cada entorno de pruebas.
Recomendaciones para las pruebas
Cuando escribas pruebas automatizadas para la reproducción de contenido multimedia, sigue estos principios básicos:
- Prefiere componentes reales o falsificaciones de Media3 en lugar de simulaciones: Evita simular
PlayeroExoPlayercon frameworks como Mockito.Playertiene invariantes de estado y contratos de objetos de escucha que los objetos simulados no aplican, lo que puede hacer que las pruebas sean inestables o que se oculten errores. En su lugar, usa una instancia deExoPlayerreal,FakePlayero simulaciones, comoFakeMediaSourceyFakeDataSource. Reserva simulaciones para verificar las invocaciones de devolución de llamada personalizadas cuando sea necesario. - Espera de forma determinista en lugar de suspender:
ExoPlayerrealiza la carga y la reproducción de contenido multimedia de forma asíncrona en un subproceso interno. Evita las llamadas arbitrarias aThread.sleep, que hacen que las pruebas sean lentas y poco confiables. UsaTestPlayerRunHelperen las pruebas de Robolectric y suspende las extensionesPlayeroPlayerFenceen las pruebas de instrumentación. Estos ayudantes esperan condiciones explícitas del reproductor y fallan de inmediato si se produce un error de reproducción. - Accede al reproductor en el subproceso de su aplicación: Se debe llamar a las instancias de
ExoPlayery a los ayudantes de espera en el subprocesoLooperde la aplicación del reproductor (por lo general, el subproceso principal). Para obtener más información, consulta los requisitos de subprocesos de ExoPlayer.
Elige el entorno de pruebas adecuado
Las diferentes capas de tu integración de contenido multimedia se benefician de distintos entornos de pruebas:
| Destino de prueba | Entorno recomendado | Utilidades clave de Media3 |
|---|---|---|
Componentes de la IU (PlayerView, IU de Compose), ViewModels o devoluciones de llamada de MediaSession |
Pruebas locales de Robolectric (src/test) o pruebas de IU instrumentadas (src/androidTest) |
FakePlayer |
Integración de ExoPlayer, transiciones de playlists, estadísticas y lógica personalizada de MediaSource o DataSource |
Pruebas locales de Robolectric (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource |
Decodificación de hardware real de MediaCodec, renderización de Surface, salida de audio y DRM |
Pruebas de instrumentación en dispositivos físicos (src/androidTest) |
ExoPlayer.Builder, Player.await* extensiones, PlayerFence |
Siempre que sea posible, ejecuta pruebas de reproducción de instrumentación en dispositivos físicos en lugar de emuladores, ya que las implementaciones de la pila de contenido multimedia del emulador varían. Para obtener más información sobre la compatibilidad con emuladores, consulta la guía de dispositivos compatibles.
Agrega dependencias de prueba
Agrega los artefactos de prueba de Media3 al bloque dependencies del archivo build.gradle.kts o build.gradle de tu módulo con la misma versión que tus otros módulos de 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"
Los dos módulos dividen las responsabilidades por entorno de ejecución:
media3-test-utils: Utilidades de varios entornos para pruebas locales y de instrumentación, incluidasTestExoPlayerBuilder,PlayerFence(y extensiones de KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestyMedia3FlagsRule.media3-test-utils-robolectric: Son utilidades para las pruebas de Robolectric en la JVM local, incluidasTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryyPlaybackOutput.
Asegúrate de que tu módulo también incluya las dependencias del framework de pruebas para tus entornos de pruebas:
- Robolectric:
org.robolectric:robolectric. Estableceandroid.testOptions.unitTests.isIncludeAndroidResources = truecuando cargues recursos locales. - AndroidX Test:
androidx.test:runner,androidx.test:coreyandroidx.test.ext:junit. - Corrutinas de Kotlin en el subproceso principal:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Acepta las APIs de prueba inestables
Las clases y los métodos de media3-test-utils y media3-test-utils-robolectric se anotan con @UnstableApi. Si bien Media3 tiene como objetivo mantener la ergonomía de los asistentes de prueba de uso generalizado en todas las versiones, las utilidades de prueba no siguen las mismas reglas de estabilidad de compatibilidad binaria que las APIs de reproducción de producción.
En tus conjuntos de orígenes de prueba, anota las clases o los métodos de prueba con
@androidx.annotation.OptIn, agrega una habilitación de package-info para tus paquetes de prueba
o configura lint.xml para las fuentes de prueba. Para obtener más información, consulta las notas de solución de problemas de lint de la API inestable.
Pruebas locales con Robolectric
Las ejecuciones de prueba locales de Robolectric se ejecutan en la JVM de tu máquina de desarrollo sin un dispositivo físico ni un emulador. Robolectric no proporciona implementaciones de MediaCodec y mantiene el Looper principal en pausa hasta que se activa de forma explícita, por lo que una instancia de ExoPlayer predeterminada no puede decodificar contenido multimedia ni entregar actualizaciones de estado por sí sola.
Crea un reproductor de prueba y avanza la reproducción
Usa TestExoPlayerBuilder para crear un ExoPlayer configurado para las pruebas de Robolectric. De forma predeterminada, TestExoPlayerBuilder configura
los siguientes componentes:
- Un
FakeClock(isAutoAdvancing = true) de avance automático que coordina la progresión del tiempo entre el subproceso de reproducción en segundo plano deExoPlayery elLooperprincipal pausado de Robolectric. - Renderizadores de audio y video falsos (
FakeVideoRendereryFakeAudioRenderer) que consumen muestras de contenido multimedia según sus marcas de tiempo sin requerir decodificadores deMediaCodec
Para avanzar en la reproducción y confirmar los resultados en el subproceso principal (con un reproductor que use Looper.getMainLooper, el valor predeterminado en Robolectric), usa los métodos estáticos advance(player) o play(player) en TestPlayerRunHelper (play(player) llama a player.play antes de esperar). En la mayoría de las condiciones, el asistente bombea el Looper principal de Robolectric hasta que se cumple la condición de destino y arroja una excepción si se produce un error de reproducción fatal o no fatal:
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); } }
En este ejemplo, asset:///media/mp4/sample.mp4 hace referencia a un archivo multimedia de muestra ubicado en el directorio src/test/assets/ (o src/main/assets/) de tu módulo.
Cuando se pasa una referencia de ExoPlayer, advance(player) y play(player), se devuelve un ExoPlayerRunResult (o PlayerRunResult cuando se pasa un Player). Estos tipos de resultados proporcionan métodos para esperar estados de reproducción, posiciones, transiciones de listas de reproducción, fotogramas renderizados, comandos pendientes y errores de reproducción esperados, o bien ignorar errores no graves. Para obtener más información sobre las condiciones de espera, consulta la
referencia de la API de TestPlayerRunHelper.
Realiza pruebas con fuentes falsas y códecs simulados
Muchas pruebas de integración verifican la navegación por la playlist, la selección de pistas, las actualizaciones del cronograma o el comportamiento de reintento, y no necesitan archivos multimedia reales. Puedes ingresar
líneas de tiempo y datos sintéticos en un reproductor TestExoPlayerBuilder con
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 usa renderizadores falsos de forma predeterminada. Si tu prueba de Robolectric
necesita interacciones de MediaCodec, configura códecs simulados y captura de salida
de la siguiente manera:
- Agrega
withAllDefaultSupportedCodecsdeShadowMediaCodecConfigcomo un@Rulede JUnit para registrar los códecs simulados de Robolectric. - Pasa un
CapturingRenderersFactorycreado con elClock(CapturingRenderersFactory(context, builder.getClock())) del compilador abuilder.setRenderersFactory. - Cuando pruebes pistas de video, adjunta un
Surface(comoSurface(SurfaceTexture(1))) conplayer.setVideoSurfacepara queMediaCodecVideoRendererrenderice los fotogramas decodificados en lugar de omitirlos. - Para verificar el resultado renderizado, llama a
register(player, capturingRenderersFactory)enPlaybackOutputantes de iniciar la reproducción. - Una vez que finalice la reproducción, libera el reproductor y la plataforma, y, luego, inspecciona el resultado con formato de
Dumper(Dumper().add(playbackOutput).toString()).
Pruebas de instrumentación en dispositivos
Las pruebas locales de Robolectric no ejecutan decodificadores de plataforma, pistas de audio, sesiones de DRM ni renderización de Surface reales. Para verificar el comportamiento de la reproducción de extremo a extremo en dispositivos que ejecutan Android, escribe pruebas de instrumentación en src/androidTest con un ExoPlayer real compilado con ExoPlayer.Builder.
En las pruebas de instrumentación de JUnit, los métodos @Test se ejecutan en un subproceso de instrumentación, en lugar del subproceso principal de la aplicación. Siempre crea, controla y libera ExoPlayer en su subproceso de aplicación Looper, por ejemplo, con runBlocking(Dispatchers.Main) en Kotlin o InstrumentationRegistry.getInstrumentation().runOnMainSync en Java.
Espera los estados del jugador con PlayerFence y corrutinas
En las pruebas de instrumentación, el Looper principal se ejecuta en tiempo real en el dispositivo.
No incluyas Robolectric ni media3-test-utils-robolectric en las dependencias de androidTestImplementation. Para esperar los estados y eventos de reproducción, media3-test-utils proporciona los siguientes ayudantes:
- Kotlin: Funciones de extensión de suspensión en
Player(comoawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedyawaitContentPositionAtLeast). - Java: El método estático
futureWhen(player)enPlayerFence, que devuelve unPlayerFencecuyos métodos de condición (comoentersPlaybackStateyisPlaying) se deben llamar en el subprocesoLooperdel reproductor y devolver unListenableFutureque el subproceso de prueba puede bloquear conget.
Ambas APIs supervisan al reproductor mientras espera. Si se produce un error de reproducción grave (o, de forma predeterminada, un error no grave) o si la espera supera el tiempo de espera (10 segundos de forma predeterminada o 30 segundos en un emulador), la llamada falla de inmediato en lugar de colgarse:
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)); } }
Cuando compilas ExoPlayer con ApplicationProvider.getApplicationContext en un módulo de app (com.android.application), los URI de asset:/// se resuelven con los recursos de la app de destino (como src/debug/assets/ o src/main/assets/, en lugar de src/androidTest/assets/).
Eventos transitorios y supervisión personalizada
Además de verificar el estado actual del reproductor (en el que las esperas se devuelven de inmediato si ya se cumple la condición), los ayudantes de media3-test-utils admiten eventos únicos y condiciones de objetos de escucha personalizados:
- Eventos transitorios, como el primer fotograma renderizado: Adjunta una salida de video (como
SurfaceoPlayerView) al reproductor y comienza a esperar conawaitFirstFrameRenderedorendersFirstFrameantes de llamar aplayer.preparepara que no se pierda un evento de fotograma único temprano. - Condiciones personalizadas: Une el objeto de escucha personalizado o las esperas de corrutinas dentro de
awaitSupervisedenplayeren Kotlin, o pasa unListenableFuturepersonalizado afutureWhen(player).withSupervision(future)en Java. Cuando pruebes la recuperación de errores no fatales, pasafailOnNonFatalErrors = false(Kotlin) o llama aignoringNonFatalErrors(Java), y personaliza los tiempos de espera contimeoutowithTimeoutMs(timeoutMs).
En el siguiente ejemplo, se espera un evento transitorio de primer fotograma y se supervisa un objeto de escucha de metadatos personalizado:
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); }); }
Prueba la IU y los componentes de la app con FakePlayer
Cuando se prueban componentes de la IU, como los elementos componibles PlayerView o media3-ui-compose, los ViewModels o las devoluciones de llamada de MediaSession, a menudo no es necesario crear un ExoPlayer completo. FakePlayer en media3-test-utils extiende SimpleBasePlayer para simular playlists, estados de reproducción, posiciones, tamaños de video, indicaciones y errores en el Looper del subproceso de llamada. Este Looper está disponible automáticamente en las pruebas de Robolectric con @RunWith(AndroidJUnit4::class) y en el subproceso principal de las pruebas de instrumentación.
De forma predeterminada, FakePlayer simula un retraso de almacenamiento en búfer de 100 ms en prepare y seekTo. Pasa bufferingDelayMs = 0 en Kotlin o establece estados directamente 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();
Utilidades de prueba adicionales
media3-test-utils incluye asistentes especializados para otras situaciones de prueba:
- Marcas de funciones y modo Canary: Usa
Media3FlagsRulejunto con@BindFlagy@BindCanaryModepara restablecer las anulaciones de marcas después de cada prueba y ejecutar pruebas parametrizadas en todos los estados de marcas. Para obtener más información, consulta Prueba con marcas de funciones. - Pruebas de contrato de
DataSourcepersonalizadas: Crea una subclase deDataSourceContractTestpara verificar que las implementaciones personalizadas deDataSourceoHttpDataSourcecumplan con las expectativas de contrato de Media3 en las solicitudes de rango, el manejo de errores y los objetos de escucha de transferencia. Para obtener más información, consulta Integra otras pilas de red.