Testes

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 Player ou ExoPlayer com frameworks como o Mockito. Player tem 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 de ExoPlayer, FakePlayer ou simulações, como FakeMediaSource e FakeDataSource. Reserve simulações para verificar invocações de callback personalizadas quando necessário.
  • Aguardar de forma determinista em vez de suspender: o ExoPlayer realiza o carregamento e a reprodução de mídia de forma assíncrona em uma linha de execução interna. Evite chamadas arbitrárias de Thread.sleep, que tornam os testes lentos e instáveis. Use TestPlayerRunHelper em testes do Robolectric e suspenda as extensões Player ou PlayerFence em 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 ExoPlayer e os auxiliares de espera precisam ser chamados na linha de execução Looper do 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:

Verifique se o módulo também inclui as dependências do framework de teste para seus ambientes de teste:

  • Robolectric: org.robolectric:robolectric. Defina android.testOptions.unitTests.isIncludeAndroidResources = true ao carregar recursos locais.
  • AndroidX Test: androidx.test:runner, androidx.test:core e androidx.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 do ExoPlayer e o Looper principal pausado do Robolectric.
  • Renderizadores de vídeo e áudio falsos (FakeVideoRenderer e FakeAudioRenderer) que consomem amostras de mídia de acordo com os carimbos de data/hora sem exigir decodificadores MediaCodec.

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:

  1. Adicione withAllDefaultSupportedCodecs de ShadowMediaCodecConfig como uma @Rule do JUnit para registrar os codecs simulados do Robolectric.
  2. Transmita um CapturingRenderersFactory criado com o Clock (CapturingRenderersFactory(context, builder.getClock())) do builder para builder.setRenderersFactory.
  3. Ao testar faixas de vídeo, anexe um Surface (como Surface(SurfaceTexture(1))) com player.setVideoSurface para que MediaCodecVideoRenderer renderize frames decodificados em vez de ignorá-los.
  4. Para verificar a saída renderizada, chame register(player, capturingRenderersFactory) em PlaybackOutput antes de iniciar a reprodução.
  5. 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:

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 Surface ou PlayerView) ao player e comece a esperar com awaitFirstFrameRendered ou rendersFirstFrame antes de chamar player.prepare para que um evento de frame único inicial não seja perdido.
  • Condições personalizadas: envolva listeners personalizados ou esperas de corrotina em awaitSupervised em player no Kotlin ou transmita um ListenableFuture personalizado para futureWhen(player).withSupervision(future) em Java. Ao testar a recuperação de erros não fatais, transmita failOnNonFatalErrors = false (Kotlin) ou chame ignoringNonFatalErrors (Java) e personalize os tempos limite com timeout ou withTimeoutMs(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 Media3FlagsRule com @BindFlag e @BindCanaryMode para 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: subclasse DataSourceContractTest para verificar se as implementações personalizadas do DataSource ou do HttpDataSource estã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.