Tests

Media3 bietet spezielle Testartefakte (media3-test-utils und media3-test-utils-robolectric), mit denen Sie die Medienwiedergabe, Übergänge des Player-Status, UI-Komponenten und benutzerdefinierte Medienquellen sowohl in lokalen JVM-Tests als auch in Geräteinstrumentierungstests testen können.

Auf dieser Seite werden empfohlene Vorgehensweisen zum Testen von Apps beschrieben, die ExoPlayer verwenden. Außerdem wird gezeigt, wie Sie die Testdienstprogramme von Media3 in den einzelnen Testumgebungen verwenden.

Empfehlungen zum Testen

Beachte beim Schreiben automatisierter Tests für die Medienwiedergabe die folgenden Grundprinzipien:

  • Echte Komponenten oder Media3-Fakes gegenüber Mocks bevorzugen: Vermeiden Sie das Mocken von Player oder ExoPlayer mit Frameworks wie Mockito. Player hat Zustandsinvarianten und Listener-Verträge, die von Mocks nicht erzwungen werden. Das kann dazu führen, dass Tests fehleranfällig sind oder Fehler nicht erkannt werden. Verwenden Sie stattdessen eine echte ExoPlayer-Instanz, FakePlayer oder Fakes wie FakeMediaSource und FakeDataSource. Reservieren Sie Mocks für die Überprüfung benutzerdefinierter Callback-Aufrufe, falls erforderlich.
  • Deterministisch warten statt schlafen: ExoPlayer führt das Laden und die Wiedergabe von Medien asynchron in einem internen Thread aus. Vermeiden Sie beliebige Thread.sleep-Aufrufe, da diese Tests langsam und unzuverlässig machen. Verwenden Sie TestPlayerRunHelper in Robolectric-Tests und die Erweiterungen Player oder PlayerFence in Instrumentierungstests. Diese Hilfsprogramme warten auf explizite Player-Bedingungen und schlagen sofort fehl, wenn ein Wiedergabefehler auftritt.
  • Auf den Player über seinen Anwendungsthread zugreifen: ExoPlayer-Instanzen und wartende Hilfsprogramme müssen im Anwendungsthread Looper des Players (normalerweise der Hauptthread) aufgerufen werden. Weitere Informationen finden Sie in den Threading-Anforderungen für ExoPlayer.

Die richtige Testumgebung auswählen

Für die verschiedenen Ebenen deiner Medienintegration sind unterschiedliche Testumgebungen sinnvoll:

Ziel testen Empfohlene Umgebung Wichtige Media3-Dienstprogramme
UI-Komponenten (PlayerView, Compose UI), ViewModels oder MediaSession-Callbacks Lokale Robolectric-Tests (src/test) oder instrumentierte UI-Tests (src/androidTest) FakePlayer
ExoPlayer-Integration, Playlist-Übergänge, Analysen und benutzerdefinierte MediaSource- oder DataSource-Logik Lokale Robolectric-Tests (src/test) TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource
Echte Hardware-MediaCodec-Decodierung, Surface-Rendering, Audioausgabe und DRM Instrumentierungstests auf physischen Geräten (src/androidTest) ExoPlayer.Builder, Player.await* Erweiterungen, PlayerFence

Führe Instrumentierungs-Wiedergabetests nach Möglichkeit auf physischen Geräten und nicht auf Emulatoren aus, da die Implementierungen des Medien-Stacks auf Emulatoren variieren. Weitere Informationen zur Emulatorunterstützung finden Sie im Leitfaden zu unterstützten Geräten.

Testabhängigkeiten hinzufügen

Füge die Media3-Testartefakte dem dependencies-Block der build.gradle.kts- oder build.gradle-Datei deines Moduls hinzu. Verwende dabei dieselbe Version wie für deine anderen Media3-Module:

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"

Die beiden Module teilen die Verantwortlichkeiten nach Laufzeitumgebung auf:

Achten Sie darauf, dass Ihr Modul auch die Test-Framework-Abhängigkeiten für Ihre Testumgebungen enthält:

  • Robolectric: org.robolectric:robolectric. Legen Sie android.testOptions.unitTests.isIncludeAndroidResources = true fest, wenn Sie lokale Assets laden.
  • AndroidX Test: androidx.test:runner, androidx.test:core und androidx.test.ext:junit.
  • Kotlin-Koroutinen im Hauptthread: org.jetbrains.kotlinx:kotlinx-coroutines-android.

Instabile Test-APIs aktivieren

Klassen und Methoden in media3-test-utils und media3-test-utils-robolectric sind mit @UnstableApi annotiert. Obwohl Media3 darauf abzielt, häufig verwendete Test-Hilfsprogramme über Releases hinweg ergonomisch zu halten, unterliegen Test-Dienstprogramme nicht denselben Stabilitätsregeln für die Binärkompatibilität wie Produktions-Wiedergabe-APIs.

Versehen Sie in Ihren Test-Source-Sets Testklassen oder -methoden mit der Annotation @androidx.annotation.OptIn, fügen Sie ein package-info-Opt-in für Ihre Testpakete hinzu oder konfigurieren Sie lint.xml für Testquellen. Weitere Informationen finden Sie in den Hinweisen zur Fehlerbehebung bei instabilen API-Lint-Fehlern.

Lokale Tests mit Robolectric

Lokale Robolectric-Tests werden auf der JVM Ihres Entwicklungscomputers ohne physisches Gerät oder Emulator ausgeführt. Robolectric bietet keine MediaCodec-Implementierungen und hält die Haupt-Looper so lange pausiert, bis sie explizit gepumpt wird. Daher kann eine Standard-ExoPlayer-Instanz keine Medien decodieren oder Statusaktualisierungen selbst bereitstellen.

Testplayer erstellen und Wiedergabe vorantreiben

Verwenden Sie TestExoPlayerBuilder, um ein ExoPlayer zu erstellen, das für Robolectric-Tests konfiguriert ist. Standardmäßig konfiguriert TestExoPlayerBuilder die folgenden Komponenten:

  • Ein automatisch fortschreitender FakeClock (isAutoAdvancing = true), der den Zeitverlauf zwischen dem Hintergrundwiedergabe-Thread von ExoPlayer und dem pausierten Haupt-Looper von Robolectric koordiniert.
  • Gefälschte Video- und Audiorenderer (FakeVideoRenderer und FakeAudioRenderer), die Medienbeispiele gemäß ihren Zeitstempeln verarbeiten, ohne dass MediaCodec-Decoder erforderlich sind.

Wenn du die Wiedergabe fortsetzen und Ergebnisse im Hauptthread bestätigen möchtest (mit einem Player, der Looper.getMainLooper verwendet, der Standardeinstellung in Robolectric), verwende die statischen Methoden advance(player) oder play(player) für TestPlayerRunHelper (play(player) ruft player.play auf, bevor gewartet wird). In den meisten Fällen pumpt der Helfer den Haupt-Looper von Robolectric, bis die Zielbedingung erfüllt ist, und löst eine Ausnahme aus, wenn ein schwerwiegender oder nicht schwerwiegender Wiedergabefehler auftritt:

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 diesem Beispiel bezieht sich asset:///media/mp4/sample.mp4 auf eine Beispiel-Mediendatei, die im Verzeichnis src/test/assets/ (oder src/main/assets/) Ihres Moduls abgelegt ist.

Wenn ein ExoPlayer-Verweis übergeben wird, geben advance(player) und play(player) ein ExoPlayerRunResult zurück (oder ein PlayerRunResult, wenn ein Player übergeben wird). Diese Ergebnistypen bieten Methoden, um auf Wiedergabestatus, Positionen, Playlist-Übergänge, gerenderte Frames, ausstehende Befehle und erwartete Wiedergabefehler zu warten oder nicht schwerwiegende Fehler zu ignorieren. Weitere Informationen zu Wartebedingungen finden Sie in der API-Referenz zu TestPlayerRunHelper.

Mit simulierten Quellen und Codecs testen

Bei vielen Integrationstests werden die Playlist-Navigation, die Titelauswahl, die Aktualisierung der Zeitachse oder das Wiederholungsverhalten überprüft. Dafür sind keine echten Mediendateien erforderlich. Sie können synthetische Zeitachsen und Daten mit FakeMediaSource, FakeTimeline, FakeDataSource oder WebServerDispatcher (mit OkHttp MockWebServer) in einen TestExoPlayerBuilder-Player einspeisen:

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 verwendet standardmäßig Fake-Renderer. Wenn für Ihren Robolectric-Test stattdessen MediaCodec-Interaktionen erforderlich sind, konfigurieren Sie simulierte Codecs und die Ausgabeerfassung so:

  1. Fügen Sie withAllDefaultSupportedCodecs aus ShadowMediaCodecConfig als JUnit-@Rule hinzu, um die simulierten Codecs von Robolectric zu registrieren.
  2. Übergeben Sie ein CapturingRenderersFactory, das mit dem Clock (CapturingRenderersFactory(context, builder.getClock())) des Builders erstellt wurde, an builder.setRenderersFactory.
  3. Hängen Sie beim Testen von Videotracks einen Surface (z. B. Surface(SurfaceTexture(1))) mit player.setVideoSurface an, damit MediaCodecVideoRenderer decodierte Frames rendert, anstatt sie zu überspringen.
  4. Wenn du die gerenderte Ausgabe prüfen möchtest, rufe register(player, capturingRenderersFactory) für PlaybackOutput auf, bevor du die Wiedergabe startest.
  5. Gib nach Abschluss der Wiedergabe den Player und die Oberfläche frei und prüfe dann die formatierte Ausgabe von Dumper (Dumper().add(playbackOutput).toString()).

Instrumentierungstests auf Geräten

Bei lokalen Robolectric-Tests werden keine echten Plattform-Decoder, Audiotracks, DRM-Sitzungen oder Surface-Rendering ausgeführt. Wenn du das End-to-End-Wiedergabeverhalten auf Geräten mit Android prüfen möchtest, schreibe Instrumentierungstests in src/androidTest mit einem echten ExoPlayer, der mit ExoPlayer.Builder erstellt wurde.

In JUnit-Instrumentierungstests werden @Test-Methoden in einem Instrumentierungsthread und nicht im Hauptthread der Anwendung ausgeführt. Erstellen, steuern und geben Sie ExoPlayer immer im Anwendungsthread Looper frei, z. B. mit runBlocking(Dispatchers.Main) in Kotlin oder InstrumentationRegistry.getInstrumentation().runOnMainSync in Java.

Mit PlayerFence und Koroutinen auf Spielerstatus warten

Bei Instrumentierungstests wird der Haupt-Looper in Echtzeit auf dem Gerät ausgeführt. Fügen Sie Robolectric oder media3-test-utils-robolectric nicht in androidTestImplementation-Abhängigkeiten ein. media3-test-utils bietet die folgenden Hilfsprogramme, um auf Wiedergabestatus und ‑ereignisse zu warten:

Beide APIs überwachen den Spieler während des Wartens. Wenn ein schwerwiegender Wiedergabefehler (oder standardmäßig ein nicht schwerwiegender Fehler) auftritt oder die Wartezeit das Zeitlimit überschreitet (standardmäßig 10 Sekunden oder 30 Sekunden auf einem Emulator), schlägt der Aufruf sofort fehl, anstatt hängen zu bleiben:

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));
  }
}

Wenn Sie ExoPlayer mit ApplicationProvider.getApplicationContext in einem App-Modul (com.android.application) erstellen, werden asset:///-URIs anhand der Assets der Ziel-App aufgelöst (z. B. src/debug/assets/ oder src/main/assets/, nicht src/androidTest/assets/).

Vorübergehende Ereignisse und benutzerdefinierte Überwachung

Zusätzlich zur Überprüfung des aktuellen Spielerstatus (wobei Wartezeiten sofort zurückgegeben werden, wenn die Bedingung bereits erfüllt ist) unterstützen die Hilfsprogramme in media3-test-utils einmalige Ereignisse und benutzerdefinierte Listener-Bedingungen:

  • Vorübergehende Ereignisse wie das erste gerenderte Frame: Hänge eine Videoausgabe (z. B. Surface oder PlayerView) an den Player an und warte mit awaitFirstFrameRendered oder rendersFirstFrame, bevor du player.prepare aufrufst, damit ein frühes einmaliges Frame-Ereignis nicht verpasst wird.
  • Benutzerdefinierte Bedingungen: Verpacken Sie benutzerdefinierte Listener oder Koroutinen-Wartezeiten in awaitSupervised auf player in Kotlin oder übergeben Sie eine benutzerdefinierte ListenableFuture an futureWhen(player).withSupervision(future) in Java. Wenn Sie die Wiederherstellung nach nicht schwerwiegenden Fehlern testen, übergeben Sie failOnNonFatalErrors = false (Kotlin) oder rufen Sie ignoringNonFatalErrors (Java) auf und passen Sie die Zeitlimits mit timeout oder withTimeoutMs(timeoutMs) an.

Im folgenden Beispiel wird auf ein vorübergehendes First-Frame-Ereignis gewartet und ein benutzerdefinierter Metadaten-Listener überwacht:

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);
      });
}

UI- und App-Komponenten mit FakePlayer testen

Wenn Sie UI-Komponenten wie PlayerView- oder media3-ui-compose-Kompositionen, ViewModels oder MediaSession-Callbacks testen, ist es oft nicht erforderlich, eine vollständige ExoPlayer zu erstellen. FakePlayer in media3-test-utils erweitert SimpleBasePlayer, um Playlists, Wiedergabestatus, Positionen, Videogrößen, Cues und Fehler im Looper des aufrufenden Threads zu simulieren. Diese Looper ist automatisch in Robolectric-Tests mit @RunWith(AndroidJUnit4::class) und im Hauptthread in Instrumentierungstests verfügbar.

Standardmäßig simuliert FakePlayer eine Pufferverzögerung von 100 ms für prepare und seekTo. Übergeben Sie bufferingDelayMs = 0 in Kotlin oder legen Sie Status direkt mit setPlaybackState fest:

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();

Zusätzliche Test-Dienstprogramme

media3-test-utils enthält spezielle Hilfsprogramme für andere Testszenarien:

  • Funktions-Flags und Canary-Modus: Verwenden Sie Media3FlagsRule zusammen mit @BindFlag und @BindCanaryMode, um Flag-Überschreibungen nach jedem Test zurückzusetzen und parametrisierte Tests für verschiedene Flag-Zustände auszuführen. Weitere Informationen finden Sie unter Mit Funktions-Flags testen.
  • Benutzerdefinierte DataSource-Vertragstests: Erstellen Sie eine Unterklasse von DataSourceContractTest, um zu prüfen, ob benutzerdefinierte DataSource- oder HttpDataSource-Implementierungen den Vertragserwartungen von Media3 in Bezug auf Bereichsanfragen, Fehlerbehandlung und Übertragungs-Listener entsprechen. Weitere Informationen finden Sie unter Andere Netzwerkstacks einbinden.