Test

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 Player o ExoPlayer con framework come Mockito. Player ha 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'istanza ExoPlayer reale, FakePlayer o falsi come FakeMediaSource e FakeDataSource. Riserva i mock per verificare le chiamate di callback personalizzate quando necessario.
  • Attendi in modo deterministico anziché sospendere: ExoPlayer esegue il caricamento e la riproduzione dei contenuti multimediali in modo asincrono su un thread interno. Evita chiamate Thread.sleep arbitrarie, che rendono i test lenti e instabili. Utilizza TestPlayerRunHelper nei test Robolectric e sospendi le estensioni Player o PlayerFence nei 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 ExoPlayer e gli helper in attesa devono essere chiamati nel thread dell'applicazione Looper del 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:

Assicurati che il modulo includa anche le dipendenze del framework di test per i tuoi ambienti di test:

  • Robolectric: org.robolectric:robolectric. Imposta android.testOptions.unitTests.isIncludeAndroidResources = true quando carichi gli asset locali.
  • AndroidX Test: androidx.test:runner, androidx.test:core e androidx.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 di ExoPlayer e il Looper principale in pausa di Robolectric.
  • Renderer video e audio fittizi (FakeVideoRenderer e FakeAudioRenderer) che utilizzano campioni multimediali in base ai relativi timestamp senza richiedere decodificatori MediaCodec.

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:

  1. Aggiungi withAllDefaultSupportedCodecs da ShadowMediaCodecConfig come @Rule JUnit per registrare i codec simulati di Robolectric.
  2. Passa un CapturingRenderersFactory creato con Clock (CapturingRenderersFactory(context, builder.getClock())) del builder a builder.setRenderersFactory.
  3. Quando testi le tracce video, collega un Surface (ad esempio Surface(SurfaceTexture(1))) con player.setVideoSurface in modo che MediaCodecVideoRenderer esegua il rendering dei frame decodificati anziché saltarli.
  4. Per verificare l'output sottoposto a rendering, chiama register(player, capturingRenderersFactory) su PlaybackOutput prima di avviare la riproduzione.
  5. 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:

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 Surface o PlayerView) al player e inizia ad attendere con awaitFirstFrameRendered o rendersFirstFrame prima di chiamare player.prepare per non perdere un evento frame iniziale una tantum.
  • Condizioni personalizzate: inserisci i listener personalizzati o le attese delle coroutine all'interno di awaitSupervised su player in Kotlin o passa un ListenableFuture personalizzato a futureWhen(player).withSupervision(future) in Java. Quando testi il ripristino da errori non irreversibili, passa failOnNonFatalErrors = false (Kotlin) o chiama ignoringNonFatalErrors (Java) e personalizza i timeout con timeout o withTimeoutMs(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 Media3FlagsRule insieme a @BindFlag e @BindCanaryMode per 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 DataSource personalizzati: sottoclasse DataSourceContractTest per verificare che le implementazioni personalizzate di DataSource o HttpDataSource siano 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.