Media3 udostępnia specjalne artefakty testowe (media3-test-utils i media3-test-utils-robolectric), które ułatwiają testowanie odtwarzania multimediów, przejść między stanami odtwarzacza, komponentów interfejsu i niestandardowych źródeł multimediów zarówno w lokalnych testach JVM, jak i w testach z instrumentacją na urządzeniu.
Na tej stronie znajdziesz zalecane metody testowania aplikacji, które używają ExoPlayer, oraz dowiesz się, jak korzystać z narzędzi testowych Media3 w każdym środowisku testowym.
Testowanie rekomendacji
Podczas pisania testów automatycznych odtwarzania multimediów postępuj zgodnie z tymi podstawowymi zasadami:
- Preferuj rzeczywiste komponenty lub atrapy Media3 zamiast obiektów imitujących: unikaj imitowania obiektów
PlayerlubExoPlayerza pomocą platform takich jak Mockito.Playerma niezmienniki stanu i umowy dotyczące detektorów, których obiekty makiety nie wymuszają, co może powodować niestabilność testów lub ukrywanie błędów. Zamiast tego użyj rzeczywistej instancjiExoPlayer,FakePlayerlub fałszywych instancji, takich jakFakeMediaSourceiFakeDataSource. W razie potrzeby zarezerwuj makiety do weryfikacji niestandardowych wywołań zwrotnych. - Czekaj deterministycznie zamiast usypiania:
ExoPlayerwczytuje i odtwarza multimedia asynchronicznie w wątku wewnętrznym. Unikaj dowolnych wywołańThread.sleep, które spowalniają testy i powodują, że są one niestabilne. UżywajTestPlayerRunHelperw testach Robolectric i rozszerzeń zawieszającychPlayerlubPlayerFencew testach z instrumentacją. Te funkcje pomocnicze czekają na określone warunki odtwarzacza i natychmiast zgłaszają błąd, jeśli wystąpi błąd odtwarzania. - Uzyskiwanie dostępu do odtwarzacza w wątku aplikacji: instancje
ExoPlayeri pomocnicze elementy oczekujące muszą być wywoływane w wątkuLooperaplikacji odtwarzacza (zwykle w głównym wątku). Więcej informacji znajdziesz w wymaganiach ExoPlayera dotyczących wątków.
Wybieranie odpowiedniego środowiska testowego
Różne warstwy integracji multimediów korzystają z różnych środowisk testowych:
| Docelowe środowisko testowe | Zalecane środowisko | Najważniejsze narzędzia Media3 |
|---|---|---|
komponenty interfejsu (PlayerView, Compose UI), ViewModels lub wywołania zwrotne MediaSession; |
Lokalne testy Robolectric (src/test) lub testy interfejsu z instrumentacją (src/androidTest) |
FakePlayer |
Integracja z ExoPlayer, przejścia playlist, statystyki i niestandardowa logika MediaSource lub DataSource |
Lokalne testy Robolectric (src/test) |
TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource |
Sprzętowe dekodowanie MediaCodec, renderowanie Surface, wyjście audio i DRM |
Testy z instrumentacją na urządzeniach fizycznych (src/androidTest) |
ExoPlayer.Builder, rozszerzenia: Player.await*, PlayerFence |
Jeśli to możliwe, uruchamiaj testy odtwarzania instrumentacji na urządzeniach fizycznych, a nie na emulatorach, ponieważ implementacje stosu multimediów w emulatorach się różnią. Więcej informacji o obsłudze emulatora znajdziesz w przewodniku po obsługiwanych urządzeniach.
Dodawanie zależności testowych
Dodaj artefakty testowe Media3 do bloku dependencies w pliku build.gradle.kts lub build.gradle modułu, używając tej samej wersji co w przypadku innych modułów 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")
Dynamiczny
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"
Te 2 moduły dzielą obowiązki według środowiska wykonawczego:
media3-test-utils: narzędzia do testów lokalnych i testów z instrumentacją w różnych środowiskach, w tymTestExoPlayerBuilder,PlayerFence(oraz rozszerzeniaPlayer.await*w języku Kotlin),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestiMedia3FlagsRule.media3-test-utils-robolectric: narzędzia do testów Robolectric na lokalnej maszynie JVM, w tymTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryiPlaybackOutput.
Upewnij się, że moduł zawiera też zależności platformy testowej dla środowisk testowych:
- Robolectric:
org.robolectric:robolectric. Podczas wczytywania zasobów lokalnych ustawandroid.testOptions.unitTests.isIncludeAndroidResources = true. - AndroidX Test:
androidx.test:runner,androidx.test:coreiandroidx.test.ext:junit. - Współprogramy Kotlin w głównym wątku:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
Akceptowanie niestabilnych interfejsów API do testowania
Klasy i metody w media3-test-utils i media3-test-utils-robolectric są oznaczone adnotacją @UnstableApi. Chociaż Media3 dąży do zachowania ergonomii powszechnie używanych pomocników testowych w różnych wersjach, narzędzia testowe nie podlegają tym samym regułom stabilności zgodności binarnej co interfejsy API odtwarzania produkcyjnego.
W testowych zbiorach źródeł dodaj do klas lub metod testowych adnotację @androidx.annotation.OptIn, dodaj akceptację package-info dla pakietów testowych lub skonfiguruj lint.xml dla źródeł testowych. Więcej informacji znajdziesz w uwagach dotyczących rozwiązywania problemów z niestabilnym interfejsem API.
Testy lokalne z Robolectric
Lokalne testy Robolectric są uruchamiane na maszynie JVM komputera używanego do programowania bez urządzenia fizycznego ani emulatora. Robolectric nie udostępnia implementacji MediaCodec i utrzymuje główny element Looper w stanie wstrzymania, dopóki nie zostanie on wyraźnie wywołany, więc domyślna instancja ExoPlayer nie może samodzielnie dekodować multimediów ani dostarczać aktualizacji stanu.
Tworzenie odtwarzacza testowego i odtwarzanie z wyprzedzeniem
Użyj TestExoPlayerBuilder, aby utworzyć ExoPlayer skonfigurowany pod kątem testów Robolectric. Domyślnie TestExoPlayerBuilder konfiguruje te komponenty:
- Automatycznie przechodzący do przodu
FakeClock(isAutoAdvancing = true), który koordynuje upływ czasu między wątkiem odtwarzania w tleExoPlayera wstrzymanym głównymLooperRobolectric. - Fałszywe mechanizmy renderowania wideo i audio (
FakeVideoRendereriFakeAudioRenderer), które wykorzystują próbki multimediów zgodnie z ich sygnaturami czasowymi bez konieczności używania dekoderówMediaCodec.
Aby kontynuować odtwarzanie i potwierdzić wyniki w głównym wątku (z odtwarzaczem używającym Looper.getMainLooper, domyślnego w Robolectric), użyj metod statycznych advance(player) lub play(player) w TestPlayerRunHelper (play(player) wywołuje player.play przed oczekiwaniem). W większości przypadków funkcja pomocnicza pompuje główny Looper Robolectric, dopóki nie zostanie spełniony warunek docelowy, i zgłasza wyjątek, jeśli wystąpi błąd odtwarzania krytyczny lub niekrytyczny:
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); } }
W tym przykładzie asset:///media/mp4/sample.mp4 odnosi się do przykładowego pliku multimedialnego umieszczonego w katalogu src/test/assets/ (lub src/main/assets/) modułu.
Po przekazaniu odwołania ExoPlayer funkcje advance(player) i play(player) zwracają obiekt ExoPlayerRunResult (lub PlayerRunResult, gdy przekazano Player). Te typy wyników udostępniają metody oczekiwania na stany odtwarzania, pozycje, przejścia playlisty, wyrenderowane klatki, oczekujące polecenia i oczekiwane błędy odtwarzania lub ignorowania niekrytycznych błędów. Więcej informacji o warunkach oczekiwania znajdziesz w dokumentacji interfejsu API TestPlayerRunHelper.
Testowanie z fałszywymi źródłami i symulowanymi kodekami
Wiele testów integracyjnych weryfikuje nawigację po playliście, wybór ścieżki, aktualizacje osi czasu lub zachowanie podczas ponawiania próby i nie wymaga prawdziwych plików multimedialnych. Możesz przekazywać syntetyczne osie czasu i dane do odtwarzacza TestExoPlayerBuilder, używając FakeMediaSource, FakeTimeline, FakeDataSource lub WebServerDispatcher (z 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 domyślnie używa fałszywych mechanizmów renderowania. Jeśli test Robolectric wymaga interakcji z MediaCodec, skonfiguruj symulowane kodeki i przechwytywanie danych wyjściowych w ten sposób:
- Dodaj
withAllDefaultSupportedCodecszShadowMediaCodecConfigjako JUnit@Rule, aby zarejestrować symulowane kodeki Robolectric. - Przekaż obiekt
CapturingRenderersFactoryutworzony za pomocą metodyClock(CapturingRenderersFactory(context, builder.getClock())) kreatora do metodybuilder.setRenderersFactory. - Podczas testowania ścieżek wideo dołącz
Surface(np.Surface(SurfaceTexture(1))) doplayer.setVideoSurface, abyMediaCodecVideoRendererrenderował zdekodowane klatki, zamiast je pomijać. - Aby sprawdzić wyrenderowane dane wyjściowe, przed rozpoczęciem odtwarzania wywołaj
register(player, capturingRenderersFactory)wPlaybackOutput. - Po zakończeniu odtwarzania zwolnij odtwarzacz i powierzchnię, a następnie sprawdź sformatowane dane wyjściowe z
Dumper(Dumper().add(playbackOutput).toString()).
Testy z instrumentacją na urządzeniach
Lokalne testy Robolectric nie wykonują rzeczywistych dekoderów platformy, ścieżek audio, sesji DRM ani renderowania Surface. Aby sprawdzić działanie odtwarzania na urządzeniach z Androidem, napisz testy z instrumentacją w src/androidTest, używając rzeczywistego ExoPlayer utworzonego za pomocą ExoPlayer.Builder.
W testach z instrumentacją JUnit metody @Test są uruchamiane w wątku instrumentacji, a nie w głównym wątku aplikacji. Zawsze twórz, kontroluj i zwalniaj obiekt ExoPlayer w wątku aplikacji Looper, na przykład za pomocą runBlocking(Dispatchers.Main) w języku Kotlin lub InstrumentationRegistry.getInstrumentation().runOnMainSync w języku Java.
Oczekiwanie na stany gracza za pomocą PlayerFence i współprogramów
W testach z instrumentacją główny Looper działa w czasie rzeczywistym na urządzeniu.
Nie uwzględniaj Robolectric ani media3-test-utils-robolectric w zależnościach androidTestImplementation. Aby oczekiwać na stany i zdarzenia odtwarzania, media3-test-utils udostępnia te funkcje pomocnicze:
- Kotlin: funkcje rozszerzeń zawieszenia w
Player(takie jakawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderediawaitContentPositionAtLeast). - Java: metoda statyczna
futureWhen(player)wPlayerFence, która zwracaPlayerFence, którego metody warunkowe (takie jakentersPlaybackStateiisPlaying) muszą być wywoływane w wątkuLooperodtwarzacza i zwracaćListenableFuture, który wątek testowy może zablokować za pomocąget.
Oba interfejsy API nadzorują odtwarzacz podczas oczekiwania. Jeśli wystąpi krytyczny błąd odtwarzania (lub domyślnie błąd niekrytyczny) albo jeśli czas oczekiwania przekroczy limit (domyślnie 10 sekund lub 30 sekund w emulatorze), wywołanie natychmiast zakończy się niepowodzeniem, zamiast się zawiesić:
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)); } }
Podczas tworzenia ExoPlayer za pomocą ApplicationProvider.getApplicationContext w module aplikacji (com.android.application) identyfikatory URI asset:/// są rozpoznawane na podstawie zasobów aplikacji docelowej (takich jak src/debug/assets/ lub src/main/assets/, a nie src/androidTest/assets/).
Zdarzenia przejściowe i nadzór niestandardowy
Oprócz sprawdzania bieżącego stanu odtwarzacza (gdzie oczekiwanie kończy się natychmiast, jeśli warunek jest już spełniony) funkcje pomocnicze w media3-test-utils obsługują zdarzenia jednorazowe i niestandardowe warunki detektora:
- Zdarzenia przejściowe, takie jak pierwsza wyrenderowana klatka: dołącz do odtwarzacza wyjście wideo (np.
SurfacelubPlayerView) i zacznij oczekiwanie za pomocąawaitFirstFrameRenderedlubrendersFirstFrameprzed wywołaniemplayer.prepare, aby nie przegapić wczesnego jednorazowego zdarzenia klatki. - Warunki niestandardowe: opakuj niestandardowy obiekt nasłuchujący lub oczekiwania współprogramu w
awaitSupervisednaplayerw Kotlin lub przekaż niestandardowy obiektListenableFuturedofutureWhen(player).withSupervision(future)w Java. Podczas testowania przywracania po błędach niekrytycznych przekażfailOnNonFatalErrors = false(Kotlin) lub wywołajignoringNonFatalErrors(Java) i dostosuj limity czasu za pomocątimeoutlubwithTimeoutMs(timeoutMs).
W tym przykładzie oczekujemy na zdarzenie przejściowe pierwszej klatki i nadzorujemy niestandardowy detektor metadanych:
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); }); }
Testowanie interfejsu i komponentów aplikacji za pomocą FakePlayer
Podczas testowania komponentów interfejsu, takich jak funkcje kompozycyjne PlayerView lub media3-ui-compose, modele widoku czy wywołania zwrotne MediaSession, tworzenie pełnego obiektu ExoPlayer jest często niepotrzebne. FakePlayer w media3-test-utils rozszerza SimpleBasePlayer, aby symulować playlisty, stany odtwarzania, pozycje, rozmiary filmów, sygnały i błędy w Looper wątku wywołującego. Ten Looper jest automatycznie dostępny w testach Robolectric z @RunWith(AndroidJUnit4::class) i w wątku głównym w testach z instrumentacją.
Domyślnie FakePlayer symuluje opóźnienie buforowania wynoszące 100 ms w prepare i seekTo. Przekaż bufferingDelayMs = 0 w języku Kotlin lub ustaw stany bezpośrednio za pomocą 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();
Dodatkowe narzędzia do testowania
media3-test-utils zawiera specjalne funkcje pomocnicze do innych scenariuszy testowania:
- Flagi funkcji i tryb Canary: używaj
Media3FlagsRulerazem z@BindFlagi@BindCanaryMode, aby resetować zastąpienia flag po każdym teście i przeprowadzać testy parametryczne w różnych stanach flag. Więcej informacji znajdziesz w artykule o testowaniu za pomocą flag funkcji. - Niestandardowe testy kontraktowe
DataSource: podklasaDataSourceContractTest, która pozwala sprawdzić, czy niestandardowe implementacjeDataSourcelubHttpDataSourcesą zgodne z oczekiwaniami Media3 dotyczącymi kontraktów w zakresie żądań zakresu, obsługi błędów i detektorów transferu. Więcej informacji znajdziesz w artykule o integracji innych stosów sieciowych.