Testowanie

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 Player lub ExoPlayer za pomocą platform takich jak Mockito. Player ma 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 instancji ExoPlayer, FakePlayer lub fałszywych instancji, takich jak FakeMediaSource i FakeDataSource. W razie potrzeby zarezerwuj makiety do weryfikacji niestandardowych wywołań zwrotnych.
  • Czekaj deterministycznie zamiast usypiania: ExoPlayer wczytuje 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żywaj TestPlayerRunHelper w testach Robolectric i rozszerzeń zawieszających Player lub PlayerFence w 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 ExoPlayer i pomocnicze elementy oczekujące muszą być wywoływane w wątku Looper aplikacji 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:

Upewnij się, że moduł zawiera też zależności platformy testowej dla środowisk testowych:

  • Robolectric: org.robolectric:robolectric. Podczas wczytywania zasobów lokalnych ustaw android.testOptions.unitTests.isIncludeAndroidResources = true.
  • AndroidX Test: androidx.test:runner, androidx.test:core i androidx.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 tle ExoPlayer a wstrzymanym głównym Looper Robolectric.
  • Fałszywe mechanizmy renderowania wideo i audio (FakeVideoRenderer i FakeAudioRenderer), które wykorzystują próbki multimediów zgodnie z ich sygnaturami czasowymi bez konieczności używania dekoderów MediaCodec.

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:

  1. Dodaj withAllDefaultSupportedCodecs z ShadowMediaCodecConfig jako JUnit @Rule, aby zarejestrować symulowane kodeki Robolectric.
  2. Przekaż obiekt CapturingRenderersFactory utworzony za pomocą metody Clock (CapturingRenderersFactory(context, builder.getClock())) kreatora do metody builder.setRenderersFactory.
  3. Podczas testowania ścieżek wideo dołącz Surface (np. Surface(SurfaceTexture(1))) do player.setVideoSurface, aby MediaCodecVideoRenderer renderował zdekodowane klatki, zamiast je pomijać.
  4. Aby sprawdzić wyrenderowane dane wyjściowe, przed rozpoczęciem odtwarzania wywołaj register(player, capturingRenderersFactory) w PlaybackOutput.
  5. 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:

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. Surface lub PlayerView) i zacznij oczekiwanie za pomocą awaitFirstFrameRendered lub rendersFirstFrame przed wywołaniem player.prepare, aby nie przegapić wczesnego jednorazowego zdarzenia klatki.
  • Warunki niestandardowe: opakuj niestandardowy obiekt nasłuchujący lub oczekiwania współprogramu w awaitSupervised na player w Kotlin lub przekaż niestandardowy obiekt ListenableFuture do futureWhen(player).withSupervision(future) w Java. Podczas testowania przywracania po błędach niekrytycznych przekaż failOnNonFatalErrors = false (Kotlin) lub wywołaj ignoringNonFatalErrors (Java) i dostosuj limity czasu za pomocą timeout lub withTimeoutMs(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 Media3FlagsRule razem z @BindFlag i @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: podklasa DataSourceContractTest, która pozwala sprawdzić, czy niestandardowe implementacje DataSource lub HttpDataSource są 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.