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
PlayeroderExoPlayermit Frameworks wie Mockito.Playerhat 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 echteExoPlayer-Instanz,FakePlayeroder Fakes wieFakeMediaSourceundFakeDataSource. Reservieren Sie Mocks für die Überprüfung benutzerdefinierter Callback-Aufrufe, falls erforderlich. - Deterministisch warten statt schlafen:
ExoPlayerführt das Laden und die Wiedergabe von Medien asynchron in einem internen Thread aus. Vermeiden Sie beliebigeThread.sleep-Aufrufe, da diese Tests langsam und unzuverlässig machen. Verwenden SieTestPlayerRunHelperin Robolectric-Tests und die ErweiterungenPlayeroderPlayerFencein 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 AnwendungsthreadLooperdes 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:
media3-test-utils: Umgebungsspezifische Dienstprogramme für lokale und Instrumentierungstests, einschließlichTestExoPlayerBuilder,PlayerFence(und Kotlin-Player.await*-Erweiterungen),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestundMedia3FlagsRule.media3-test-utils-robolectric: Dienstprogramme für Robolectric-Tests auf der lokalen JVM, einschließlichTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryundPlaybackOutput.
Achten Sie darauf, dass Ihr Modul auch die Test-Framework-Abhängigkeiten für Ihre Testumgebungen enthält:
- Robolectric:
org.robolectric:robolectric. Legen Sieandroid.testOptions.unitTests.isIncludeAndroidResources = truefest, wenn Sie lokale Assets laden. - AndroidX Test:
androidx.test:runner,androidx.test:coreundandroidx.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 vonExoPlayerund dem pausierten Haupt-Loopervon Robolectric koordiniert. - Gefälschte Video- und Audiorenderer (
FakeVideoRendererundFakeAudioRenderer), die Medienbeispiele gemäß ihren Zeitstempeln verarbeiten, ohne dassMediaCodec-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:
- Fügen Sie
withAllDefaultSupportedCodecsausShadowMediaCodecConfigals JUnit-@Rulehinzu, um die simulierten Codecs von Robolectric zu registrieren. - Übergeben Sie ein
CapturingRenderersFactory, das mit demClock(CapturingRenderersFactory(context, builder.getClock())) des Builders erstellt wurde, anbuilder.setRenderersFactory. - Hängen Sie beim Testen von Videotracks einen
Surface(z. B.Surface(SurfaceTexture(1))) mitplayer.setVideoSurfacean, damitMediaCodecVideoRendererdecodierte Frames rendert, anstatt sie zu überspringen. - Wenn du die gerenderte Ausgabe prüfen möchtest, rufe
register(player, capturingRenderersFactory)fürPlaybackOutputauf, bevor du die Wiedergabe startest. - 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:
- Kotlin: Suspend-Erweiterungsfunktionen für
Player(z. B.awaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedundawaitContentPositionAtLeast). - Java: Die statische
futureWhen(player)-Methode fürPlayerFence, die einPlayerFencezurückgibt, dessen Bedingungsmethoden (z. B.entersPlaybackStateundisPlaying) imLooper-Thread des Players aufgerufen werden müssen und einListenableFuturezurückgeben, das der Testthread mitgetblockieren kann.
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.
SurfaceoderPlayerView) an den Player an und warte mitawaitFirstFrameRenderedoderrendersFirstFrame, bevor duplayer.prepareaufrufst, damit ein frühes einmaliges Frame-Ereignis nicht verpasst wird. - Benutzerdefinierte Bedingungen: Verpacken Sie benutzerdefinierte Listener oder Koroutinen-Wartezeiten in
awaitSupervisedaufplayerin Kotlin oder übergeben Sie eine benutzerdefinierteListenableFutureanfutureWhen(player).withSupervision(future)in Java. Wenn Sie die Wiederherstellung nach nicht schwerwiegenden Fehlern testen, übergeben SiefailOnNonFatalErrors = false(Kotlin) oder rufen SieignoringNonFatalErrors(Java) auf und passen Sie die Zeitlimits mittimeoutoderwithTimeoutMs(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
Media3FlagsRulezusammen mit@BindFlagund@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 vonDataSourceContractTest, um zu prüfen, ob benutzerdefinierteDataSource- oderHttpDataSource-Implementierungen den Vertragserwartungen von Media3 in Bezug auf Bereichsanfragen, Fehlerbehandlung und Übertragungs-Listener entsprechen. Weitere Informationen finden Sie unter Andere Netzwerkstacks einbinden.