Test

Media3, hem yerel JVM testlerinde hem de cihaz enstrümantasyon testlerinde medya oynatmayı, oynatıcı durum geçişlerini, kullanıcı arayüzü bileşenlerini ve özel medya kaynaklarını test etmenize yardımcı olmak için özel test yapıları (media3-test-utils ve media3-test-utils-robolectric) sağlar.

Bu sayfada, ExoPlayer kullanan uygulamaları test etmek için önerilen uygulamalar açıklanmakta ve her test ortamında Media3'ün test yardımcı programlarının nasıl kullanılacağı gösterilmektedir.

Önerileri test etme

Medya oynatma için otomatik testler yazarken şu temel ilkelere uyun:

  • Taklitler yerine gerçek bileşenleri veya Media3 sahtelerini tercih edin: Mockito gibi çerçevelerle Player veya ExoPlayer'yi taklit etmekten kaçının. Player, taklitlerin zorunlu kılmadığı durum sabitlerine ve dinleyici sözleşmelerine sahiptir. Bu durum, testlerin kırılgan olmasına veya hataların gizlenmesine neden olabilir. Bunun yerine gerçek bir ExoPlayer örneği, FakePlayer veya FakeMediaSource ve FakeDataSource gibi sahte örnekler kullanın. Taklitleri, gerektiğinde özel geri çağırma çağrılarını doğrulamak için kullanın.
  • Bekleme yerine deterministik bekleme kullanın: ExoPlayer, medya yükleme ve oynatma işlemlerini dahili bir iş parçacığında eşzamansız olarak gerçekleştirir. Testleri yavaş ve güvenilmez hale getiren rastgele Thread.sleep çağrılarından kaçının. Robolectric testlerinde TestPlayerRunHelper, enstrümantasyon testlerinde ise askıya alma Player uzantıları veya PlayerFence kullanın. Bu yardımcılar, belirli oyuncu koşullarını bekler ve oynatma hatası oluşursa hemen başarısız olur.
  • Oynatıcıya uygulama iş parçacığında erişme: ExoPlayer örnekleri ve bekleyen yardımcılar, oynatıcının uygulama iş parçacığında Looper (genellikle ana iş parçacığı) çağrılmalıdır. Daha fazla bilgi için ExoPlayer iş parçacığı oluşturma gereksinimleri bölümüne bakın.

Doğru test ortamını seçme

Medya entegrasyonunuzun farklı katmanları, farklı test ortamlarından yararlanır:

Test hedefi Önerilen ortam Temel Media3 yardımcı programları
Kullanıcı arayüzü bileşenleri (PlayerView, Compose kullanıcı arayüzü), ViewModel'ler veya MediaSession geri çağırmaları Yerel Robolectric testleri (src/test) veya araçlı kullanıcı arayüzü testleri (src/androidTest) FakePlayer
ExoPlayer entegrasyonu, oynatma listesi geçişleri, analizler ve özel MediaSource veya DataSource mantığı Yerel Robolectric testleri (src/test) TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource
Gerçek donanım MediaCodec kod çözme, Surface oluşturma, ses çıkışı ve DRM Fiziksel cihazlarda enstrümantasyon testleri (src/androidTest) ExoPlayer.Builder, Player.await* uzantı, PlayerFence

Emülatör medya yığını uygulamaları farklılık gösterdiğinden, mümkün olduğunda araçlı oynatma testlerini emülatörler yerine fiziksel cihazlarda çalıştırın. Emülatör desteği hakkında daha fazla bilgi için desteklenen cihazlar kılavuzuna bakın.

Test bağımlılıkları ekleme

Media3 test yapılarını, diğer Media3 modüllerinizle aynı sürümü kullanarak modülünüzün build.gradle.kts veya build.gradle dosyasının dependencies bloğuna ekleyin:

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")

Modern

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"

İki modül, sorumlulukları çalışma zamanı ortamına göre böler:

Modülünüzün, test ortamlarınız için test çerçevesi bağımlılıklarını da içerdiğinden emin olun:

  • Robolectric: org.robolectric:robolectric. Yerel öğeler yüklenirken android.testOptions.unitTests.isIncludeAndroidResources = true değerini ayarlayın.
  • AndroidX Test: androidx.test:runner, androidx.test:core ve androidx.test.ext:junit.
  • Ana iş parçacığındaki Kotlin eş yordamları: org.jetbrains.kotlinx:kotlinx-coroutines-android.

Kararsız test API'lerini etkinleştirme

media3-test-utils ve media3-test-utils-robolectric içindeki sınıflar ve yöntemler @UnstableApi ile ek açıklama eklenir. Media3, yaygın olarak kullanılan test yardımcılarını sürümler arasında ergonomik tutmayı amaçlasa da test yardımcı programları, üretim oynatma API'leriyle aynı ikili uyumluluk kararlılık kurallarına uymaz.

Test kaynak gruplarınızda, test sınıflarına veya yöntemlerine @androidx.annotation.OptIn ile ek açıklama ekleyin, test paketleriniz için package-info etkinleştirmesi ekleyin veya test kaynakları için lint.xml yapılandırın. Daha fazla bilgi için kararsız API lint sorun giderme notlarına bakın.

Robolectric ile yerel testler

Yerel Robolectric test çalıştırmaları, fiziksel cihaz veya emülatör olmadan geliştirme makinenizin JVM'sinde çalışır. Robolectric, MediaCodec uygulamaları sağlamaz ve ana Looper'i açıkça pompalanana kadar duraklatılmış olarak tutar. Bu nedenle, varsayılan bir ExoPlayer örneği medyayı çözemez veya durum güncellemelerini kendi başına iletemez.

Test oynatıcı oluşturma ve oynatmayı ilerletme

Robolectric testleri için yapılandırılmış bir ExoPlayer oluşturmak üzere TestExoPlayerBuilder kullanın. Varsayılan olarak TestExoPlayerBuilder, aşağıdaki bileşenleri yapılandırır:

  • ExoPlayer'nin arka planda oynatma iş parçacığı ile Robolectric'in duraklatılmış ana Looper arasında zaman ilerlemesini koordine eden, otomatik ilerleyen bir FakeClock (isAutoAdvancing = true).
  • MediaCodec kod çözücüleri gerektirmeden medya örneklerini zaman damgalarına göre kullanan sahte video ve ses oluşturucular (FakeVideoRenderer ve FakeAudioRenderer).

Oynatmayı ilerletmek ve ana iş parçacığında sonuçları onaylamak için (Robolectric'te varsayılan olan Looper.getMainLooper kullanan bir oynatıcıyla) TestPlayerRunHelper üzerinde statik advance(player) veya play(player) yöntemlerini kullanın (play(player), beklemeden önce player.play yöntemini çağırır). Çoğu koşulda yardımcı, hedef koşul karşılanana kadar Robolectric'in ana Looper öğesini pompalar ve önemli veya önemli olmayan bir oynatma hatası oluşursa istisna oluşturur:

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

Bu örnekte asset:///media/mp4/sample.mp4, modülünüzün src/test/assets/ (veya src/main/assets/) dizinine yerleştirilmiş örnek bir medya dosyasını ifade eder.

Bir ExoPlayer referansı iletildiğinde advance(player) ve play(player), bir ExoPlayerRunResult (veya bir Player iletildiğinde PlayerRunResult) döndürür. Bu sonuç türleri; oynatma durumlarını, konumları, oynatma listesi geçişlerini, oluşturulan kareleri, bekleyen komutları ve beklenen oynatma hatalarını bekleme veya önemli olmayan hataları yoksayma yöntemleri sağlar. Bekleme koşulları hakkında daha fazla bilgi için TestPlayerRunHelper API referansına bakın.

Sahte kaynaklar ve simüle edilmiş codec'lerle test etme

Birçok entegrasyon testi; oynatma listesinde gezinme, parça seçimi, zaman çizelgesi güncellemeleri veya yeniden deneme davranışını doğrular ve gerçek medya dosyalarına ihtiyaç duymaz. FakeMediaSource, FakeTimeline, FakeDataSource veya WebServerDispatcher (OkHttp MockWebServer ile) kullanarak sentetik zaman çizelgelerini ve verileri bir TestExoPlayerBuilder oynatıcıya aktarabilirsiniz:

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, varsayılan olarak sahte oluşturucular kullanır. Robolectric testinizde MediaCodec etkileşimleri gerekiyorsa simüle edilmiş codec'leri ve çıkış yakalamayı aşağıdaki gibi yapılandırın:

  1. Robolectric'in simüle edilmiş codec'lerini kaydetmek için ShadowMediaCodecConfig'dan withAllDefaultSupportedCodecs öğesini JUnit @Rule olarak ekleyin.
  2. Oluşturucunun Clock (CapturingRenderersFactory(context, builder.getClock())) ile oluşturulmuş bir CapturingRenderersFactory öğesini builder.setRenderersFactory öğesine iletin.
  3. Video kanallarını test ederken player.setVideoSurface ile bir Surface (ör. Surface(SurfaceTexture(1))) ekleyin. Böylece MediaCodecVideoRenderer, kodu çözülmüş kareleri atlamak yerine oluşturur.
  4. Oluşturulan çıkışı doğrulamak için oynatmayı başlatmadan önce PlaybackOutput üzerinde register(player, capturingRenderersFactory) yöntemini çağırın.
  5. Oynatma bittikten sonra oynatıcıyı ve yüzeyi serbest bırakın, ardından Dumper (Dumper().add(playbackOutput).toString()) biçimlendirilmiş çıkışını inceleyin.

Cihazlarda enstrümantasyon testleri

Yerel Robolectric testleri gerçek platform kod çözücülerini, ses parçalarını, DRM oturumlarını veya Surface oluşturmayı yürütmez. Android çalıştıran cihazlarda uçtan uca oynatma davranışını doğrulamak için ExoPlayer.Builder ile oluşturulmuş gerçek bir ExoPlayer kullanarak src/androidTest içinde enstrümantasyon testleri yazın.

JUnit enstrümantasyon testlerinde @Test yöntemleri, uygulamanın ana iş parçacığı yerine bir enstrümantasyon iş parçacığında çalışır. ExoPlayer öğesini her zaman uygulama Looper iş parçacığında oluşturun, kontrol edin ve serbest bırakın. Örneğin, Kotlin'de runBlocking(Dispatchers.Main) veya Java'da InstrumentationRegistry.getInstrumentation().runOnMainSync kullanın.

PlayerFence ve eş yordamlarla oyuncu durumlarını bekleme

Enstrümantasyon testlerinde ana Looper, cihazda gerçek zamanlı olarak çalışır. androidTestImplementation bağımlılıklarına Robolectric veya media3-test-utils-robolectric eklemeyin. media3-test-utils, oynatma durumlarını ve etkinliklerini beklemek için aşağıdaki yardımcıları sağlar:

  • Kotlin: Player üzerinde askıya alma uzantısı işlevleri (ör. awaitPlaybackState, awaitIsPlaying, awaitFirstFrameRendered ve awaitContentPositionAtLeast).
  • Java: PlayerFence üzerindeki statik futureWhen(player) yöntemi, koşul yöntemleri (entersPlaybackState ve isPlaying gibi) oynatıcının Looper iş parçacığında çağrılması gereken bir PlayerFence döndürür ve test iş parçacığının get ile engelleyebileceği bir ListenableFuture döndürür.

Her iki API de oyuncuyu beklerken denetler. Önemli bir oynatma hatası (veya varsayılan olarak önemli olmayan bir hata) oluşursa ya da bekleme süresi zaman aşımını (varsayılan olarak 10 saniye veya emülatörde 30 saniye) aşarsa çağrı askıda kalmak yerine hemen başarısız olur:

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

Bir uygulama modülünde (com.android.application) ApplicationProvider.getApplicationContext ile ExoPlayer oluştururken asset:/// URI'leri, hedef uygulamanın öğelerine (src/androidTest/assets/ yerine src/debug/assets/ veya src/main/assets/ gibi) göre çözümlenir.

Geçici etkinlikler ve özel gözetim

media3-test-utils içindeki yardımcılar, mevcut oyuncu durumunu kontrol etmenin yanı sıra (koşul zaten karşılanmışsa bekleme işlemleri hemen döndürülür) tek seferlik etkinlikleri ve özel dinleyici koşullarını destekler:

  • Oluşturulan ilk kare gibi geçici etkinlikler: Oynatıcıya bir video çıkışı (ör. Surface veya PlayerView) ekleyin ve player.prepare yöntemini çağırmadan önce awaitFirstFrameRendered ya da rendersFirstFrame ile beklemeye başlayın. Böylece erken bir tek seferlik kare etkinliği kaçırılmaz.
  • Özel koşullar: Kotlin'de player üzerinde awaitSupervised içinde özel dinleyici veya eş yordam bekleme sürelerini sarmalayın ya da Java'da futureWhen(player).withSupervision(future)'e özel bir ListenableFuture iletin. Önemli olmayan hatalardan kurtarma testini yaparken failOnNonFatalErrors = false (Kotlin) veya ignoringNonFatalErrors (Java) çağrısını iletin ve zaman aşımlarını timeout ya da withTimeoutMs(timeoutMs) ile özelleştirin.

Aşağıdaki örnekte, geçici bir ilk kare etkinliği beklenir ve özel bir meta veri dinleyicisi denetlenir:

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

FakePlayer ile kullanıcı arayüzünü ve uygulama bileşenlerini test etme

PlayerView veya media3-ui-compose bileşenleri, ViewModel'ler ya da MediaSession geri çağırmaları gibi kullanıcı arayüzü bileşenlerini test ederken tam bir ExoPlayer oluşturmak genellikle gereksizdir. media3-test-utils içindeki FakePlayer, çağıran iş parçacığının Looper öğesinde oynatma listelerini, oynatma durumlarını, konumları, video boyutlarını, işaretleri ve hataları simüle etmek için SimpleBasePlayer öğesini genişletir. Bu Looper, @RunWith(AndroidJUnit4::class) ile Robolectric testlerinde ve enstrümantasyon testlerindeki ana iş parçacığında otomatik olarak kullanılabilir.

Varsayılan olarak FakePlayer, prepare ve seekTo üzerinde 100 ms'lik bir arabelleğe alma gecikmesini simüle eder. Kotlin'de bufferingDelayMs = 0 değerini iletin veya durumları doğrudan setPlaybackState ile ayarlayın:

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

Ek test yardımcı programları

media3-test-utils, diğer test senaryoları için özel yardımcılar içerir:

  • Özellik bayrakları ve Canary modu: Her testten sonra bayrak geçersiz kılma işlemlerini sıfırlamak ve bayrak durumları genelinde parametreleştirilmiş testler çalıştırmak için Media3FlagsRule'ı @BindFlag ve @BindCanaryMode ile birlikte kullanın. Daha fazla bilgi için özellik bayraklarıyla test etme başlıklı makaleyi inceleyin.
  • Özel DataSource sözleşme testleri: Özel DataSource veya HttpDataSource uygulamalarının aralık istekleri, hata işleme ve aktarım dinleyicileri genelinde Media3'ün sözleşme beklentilerine uygun olduğunu doğrulamak için DataSourceContractTest alt sınıfını kullanın. Daha fazla bilgi için Diğer ağ yığınlarını entegre etme başlıklı makaleyi inceleyin.