O Media3 oferece artefatos de teste dedicados (media3-test-utils e
media3-test-utils-robolectric) para ajudar você a testar a reprodução de mídia, as transições de estado do player,
os componentes de interface e as fontes de mídia personalizadas em testes de JVM locais
e testes de instrumentação de dispositivos.
Nesta página, descrevemos as práticas recomendadas para testar apps que usam ExoPlayer
e mostramos como usar os utilitários de teste do Media3 em cada ambiente de teste.
Recomendações de teste
Ao escrever testes automatizados para reprodução de mídia, siga estes princípios básicos:
- Dê preferência a componentes reais ou fakes do Media3 em vez de simulações: evite simular
PlayerouExoPlayercom frameworks como o Mockito.Playertem invariantes de estado e contratos de listener que as simulações não aplicam, o que pode tornar os testes frágeis ou ocultar bugs. Em vez disso, use uma instância real deExoPlayer,FakePlayerou simulações, comoFakeMediaSourceeFakeDataSource. Reserve simulações para verificar invocações de callback personalizadas quando necessário. - Aguardar de forma determinista em vez de suspender: o
ExoPlayerrealiza o carregamento e a reprodução de mídia de forma assíncrona em uma linha de execução interna. Evite chamadas arbitrárias deThread.sleep, que tornam os testes lentos e instáveis. UseTestPlayerRunHelperem testes do Robolectric e suspenda as extensõesPlayerouPlayerFenceem testes de instrumentação. Esses auxiliares aguardam condições explícitas do player e falham imediatamente se ocorrer um erro de reprodução. - Acesse o player na linha de execução do aplicativo: as instâncias de
ExoPlayere os auxiliares de espera precisam ser chamados na linha de execuçãoLooperdo aplicativo do player (geralmente a linha de execução principal). Para mais informações, consulte os requisitos de threading do ExoPlayer.
Escolher o ambiente de teste certo
Diferentes camadas da sua integração de mídia se beneficiam de diferentes ambientes de teste:
| Destino do teste | Ambiente recomendado | Principais utilitários do Media3 |
|---|---|---|
Componentes de interface (PlayerView, Compose UI), ViewModels ou callbacks MediaSession |
Testes locais do Robolectric (src/test) ou testes de interface instrumentados (src/androidTest) |
FakePlayer |
Integração com o ExoPlayer, transições de playlist, análise de dados e lógica personalizada de MediaSource ou DataSource |
Testes locais do Robolectric (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource |
Decodificação MediaCodec, renderização Surface, saída de áudio e DRM de hardware real |
Testes de instrumentação em dispositivos físicos (src/androidTest) |
ExoPlayer.Builder, Player.await* extensões, PlayerFence |
Sempre que possível, execute testes de reprodução de instrumentação em dispositivos físicos, em vez de emuladores, porque as implementações de pilha de mídia do emulador variam. Para mais informações sobre o suporte ao emulador, consulte o guia de dispositivos compatíveis.
Adicionar dependências de teste
Adicione os artefatos de teste do Media3 ao bloco dependencies do arquivo build.gradle.kts ou build.gradle do módulo usando a mesma versão dos outros módulos do 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"
Os dois módulos dividem as responsabilidades por ambiente de execução:
media3-test-utils: utilitários de vários ambientes para testes locais e instrumentados, incluindoTestExoPlayerBuilder,PlayerFence(e extensões KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTesteMedia3FlagsRule.media3-test-utils-robolectric: utilitários para testes do Robolectric na JVM local, incluindoTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryePlaybackOutput.
Verifique se o módulo também inclui as dependências do framework de teste para seus ambientes de teste:
- Robolectric:
org.robolectric:robolectric. Definaandroid.testOptions.unitTests.isIncludeAndroidResources = trueao carregar recursos locais. - AndroidX Test:
androidx.test:runner,androidx.test:coreeandroidx.test.ext:junit. - Corrotinas do Kotlin na linha de execução principal:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Ativar APIs de teste instáveis
As classes e os métodos em media3-test-utils e media3-test-utils-robolectric
são anotados com @UnstableApi. Embora o Media3 tenha como objetivo manter
os auxiliares de teste amplamente usados ergonômicos em todas as versões, os utilitários de teste não seguem
as mesmas regras de estabilidade de compatibilidade binária que as APIs de reprodução de produção.
Nos conjuntos de origem de teste, anote as classes ou os métodos de teste com
@androidx.annotation.OptIn, adicione uma ativação de package-info para os pacotes
de teste ou configure lint.xml para as origens de teste. Para mais informações, consulte
as observações sobre solução de problemas de lint da API instável.
Testes locais com o Robolectric
As execuções de teste locais do Robolectric são executadas na JVM da máquina de desenvolvimento sem um dispositivo físico ou emulador. O Robolectric não fornece
implementações de MediaCodec e mantém o Looper principal pausado até
que seja explicitamente bombeado. Portanto, uma instância padrão de ExoPlayer não pode decodificar mídia ou
enviar atualizações de estado por conta própria.
Criar um player de teste e avançar a reprodução
Use TestExoPlayerBuilder para criar um ExoPlayer
configurado para testes do Robolectric. Por padrão, o TestExoPlayerBuilder configura
os seguintes componentes:
- Um
FakeClock(isAutoAdvancing = true) de avanço automático que coordena a progressão de tempo entre a linha de execução de reprodução em segundo plano doExoPlayere oLooperprincipal pausado do Robolectric. - Renderizadores de vídeo e áudio falsos (
FakeVideoRenderereFakeAudioRenderer) que consomem amostras de mídia de acordo com os carimbos de data/hora sem exigir decodificadoresMediaCodec.
Para avançar a reprodução e declarar resultados na linha de execução principal (com um player usando
Looper.getMainLooper, o padrão no Robolectric), use os métodos estáticos
advance(player) ou play(player) em
TestPlayerRunHelper (play(player) chama
player.play antes de esperar). Para a maioria das condições, o auxiliar bombeia
o Looper principal do Robolectric até que a condição de destino seja atendida e gera uma
exceção se ocorrer um erro de reprodução fatal ou não 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); } }
Neste exemplo, asset:///media/mp4/sample.mp4 se refere a um arquivo de mídia de amostra
colocado no diretório src/test/assets/ (ou src/main/assets/) do módulo.
Quando uma referência ExoPlayer é transmitida, advance(player) e play(player)
retornam um ExoPlayerRunResult (ou
PlayerRunResult quando um Player é transmitido). Esses tipos de resultado
fornecem métodos para aguardar estados de reprodução, posições, transições de playlist,
frames renderizados, comandos pendentes e erros de reprodução esperados ou
ignorar erros não fatais. Para mais informações sobre condições de espera, consulte a
referência da API TestPlayerRunHelper.
Testar com fontes falsas e codecs simulados
Muitos testes de integração verificam a navegação na playlist, a seleção de faixas, as atualizações
da linha do tempo ou o comportamento de nova tentativa e não precisam de arquivos de mídia reais. É possível alimentar
linhas do tempo e dados sintéticos em um player do TestExoPlayerBuilder usando
FakeMediaSource, FakeTimeline,
FakeDataSource ou
WebServerDispatcher (com o 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();
O TestExoPlayerBuilder usa renderizadores falsos por padrão. Se o teste do Robolectric
precisar de interações de MediaCodec, configure codecs simulados e captura de saída
da seguinte maneira:
- Adicione
withAllDefaultSupportedCodecsdeShadowMediaCodecConfigcomo uma@Ruledo JUnit para registrar os codecs simulados do Robolectric. - Transmita um
CapturingRenderersFactorycriado com oClock(CapturingRenderersFactory(context, builder.getClock())) do builder parabuilder.setRenderersFactory. - Ao testar faixas de vídeo, anexe um
Surface(comoSurface(SurfaceTexture(1))) complayer.setVideoSurfacepara queMediaCodecVideoRendererrenderize frames decodificados em vez de ignorá-los. - Para verificar a saída renderizada, chame
register(player, capturingRenderersFactory)emPlaybackOutputantes de iniciar a reprodução. - Depois que a reprodução terminar, libere o player e a superfície e inspecione
a saída formatada de
Dumper(Dumper().add(playbackOutput).toString()).
Testes de instrumentação em dispositivos
Os testes locais do Robolectric não executam decodificadores de plataforma reais, faixas de áudio, sessões de DRM
ou renderização de Surface. Para verificar o comportamento de reprodução de ponta a ponta em
dispositivos que executam Android, escreva testes de instrumentação em
src/androidTest usando um ExoPlayer real criado com ExoPlayer.Builder.
Nos testes de instrumentação do JUnit, os métodos @Test são executados em uma linha de execução de instrumentação,
e não na linha de execução principal do aplicativo. Sempre crie, controle e
libere o ExoPlayer na linha de execução Looper do aplicativo, por exemplo, usando
runBlocking(Dispatchers.Main) em Kotlin ou
InstrumentationRegistry.getInstrumentation().runOnMainSync em Java.
Aguardar estados do jogador com PlayerFence e corrotinas
Nos testes de instrumentação, o Looper principal é executado em tempo real no dispositivo.
Não inclua o Robolectric ou media3-test-utils-robolectric nas dependências de
androidTestImplementation. Para aguardar estados e eventos de
reprodução, o media3-test-utils oferece os seguintes auxiliares:
- Kotlin: suspensão de funções de extensão em
Player(comoawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedeawaitContentPositionAtLeast). - Java: o método estático
futureWhen(player)emPlayerFence, que retorna umPlayerFencecom métodos de condição (comoentersPlaybackStateeisPlaying) que precisam ser chamados na linha de execuçãoLooperdo player e retornar umListenableFutureque a linha de execução de teste pode bloquear comget.
As duas APIs supervisionam o jogador enquanto ele espera. Se ocorrer um erro fatal de reprodução (ou, por padrão, um erro não fatal) ou se a espera exceder o tempo limite (10 segundos por padrão ou 30 segundos em um emulador), a chamada vai falhar imediatamente, em vez de travar:
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)); } }
Ao criar ExoPlayer com ApplicationProvider.getApplicationContext em
um módulo de app (com.android.application), os URIs asset:/// são resolvidos em relação aos
recursos do app de destino (como src/debug/assets/ ou src/main/assets/, em vez
de src/androidTest/assets/).
Eventos transitórios e supervisão personalizada
Além de verificar o estado atual do player (em que as esperas retornam imediatamente se
a condição já for atendida), os auxiliares em media3-test-utils são compatíveis com
eventos únicos e condições de listener personalizadas:
- Eventos temporários, como o primeiro frame renderizado: anexe uma saída de vídeo
(como
SurfaceouPlayerView) ao player e comece a esperar comawaitFirstFrameRenderedourendersFirstFrameantes de chamarplayer.preparepara que um evento de frame único inicial não seja perdido. - Condições personalizadas: envolva listeners personalizados ou esperas de corrotina em
awaitSupervisedemplayerno Kotlin ou transmita umListenableFuturepersonalizado parafutureWhen(player).withSupervision(future)em Java. Ao testar a recuperação de erros não fatais, transmitafailOnNonFatalErrors = false(Kotlin) ou chameignoringNonFatalErrors(Java) e personalize os tempos limite comtimeoutouwithTimeoutMs(timeoutMs).
O exemplo a seguir aguarda um evento temporário de primeiro frame e supervisiona um listener de metadados personalizados:
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); }); }
Testar a interface e os componentes do app com o FakePlayer
Ao testar componentes de interface, como elementos combináveis PlayerView ou media3-ui-compose,
ViewModels ou callbacks MediaSession, geralmente não é necessário criar um
ExoPlayer completo. FakePlayer em
media3-test-utils estende SimpleBasePlayer para
simular playlists, estados de reprodução, posições, tamanhos de vídeo, marcações e erros no
Looper da linha de execução de chamada. Esse Looper fica disponível automaticamente em
testes do Robolectric com @RunWith(AndroidJUnit4::class) e na linha de execução principal
em testes de instrumentação.
Por padrão, FakePlayer simula um atraso de buffer de 100 ms em prepare e
seekTo. Transmita bufferingDelayMs = 0 em Kotlin ou defina estados diretamente com
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();
Outros utilitários de teste
O media3-test-utils inclui auxiliares especializados para outros cenários de teste:
- Flags de recursos e modo canário: use
Media3FlagsRulecom@BindFlage@BindCanaryModepara redefinir substituições de flags após cada teste e executar testes parametrizados em estados de flags. Para mais informações, consulte Como testar com flags de recurso. - Testes de contrato personalizados do
DataSource: subclasseDataSourceContractTestpara verificar se as implementações personalizadas doDataSourceou doHttpDataSourceestão em conformidade com as expectativas de contrato do Media3 em solicitações de intervalo, tratamento de erros e listeners de transferência. Para mais informações, consulte Como integrar outras pilhas de rede.