Pengujian

Media3 menyediakan artefak pengujian khusus (media3-test-utils dan media3-test-utils-robolectric) untuk membantu Anda menguji pemutaran media, transisi status pemutar, komponen UI, dan sumber media kustom dalam pengujian JVM lokal dan uji instrumentasi perangkat.

Halaman ini menguraikan praktik yang direkomendasikan untuk menguji aplikasi yang menggunakan ExoPlayer dan menunjukkan cara menggunakan utilitas pengujian Media3 di setiap lingkungan pengujian.

Rekomendasi pengujian

Saat menulis pengujian otomatis untuk pemutaran media, ikuti prinsip inti berikut:

  • Lebih memilih komponen nyata atau Media3 palsu daripada tiruan: Hindari meniru Player atau ExoPlayer dengan framework seperti Mockito. Player memiliki invarian status dan kontrak pemroses yang tidak diterapkan oleh tiruan, yang dapat membuat pengujian menjadi rapuh atau menyembunyikan bug. Sebagai gantinya, gunakan instance ExoPlayer yang sebenarnya, FakePlayer, atau palsu seperti FakeMediaSource dan FakeDataSource. Cadangkan tiruan untuk memverifikasi pemanggilan callback kustom jika diperlukan.
  • Menunggu secara deterministik, bukan tidur: ExoPlayer melakukan pemuatan dan pemutaran media secara asinkron pada thread internal. Hindari panggilan Thread.sleep arbitrer, yang membuat pengujian menjadi lambat dan tidak stabil. Gunakan TestPlayerRunHelper dalam pengujian Robolectric dan menangguhkan ekstensi Player atau PlayerFence dalam uji instrumentasi. Helper ini menunggu kondisi pemutar yang eksplisit dan segera gagal jika terjadi error pemutaran.
  • Mengakses pemutar di thread aplikasinya: Instance ExoPlayer dan helper tunggu harus dipanggil di thread Looper aplikasi pemutar (biasanya thread utama). Untuk informasi selengkapnya, lihat persyaratan threading ExoPlayer.

Memilih lingkungan pengujian yang tepat

Berbagai lapisan integrasi media Anda mendapatkan manfaat dari lingkungan pengujian yang berbeda:

Target pengujian Lingkungan yang direkomendasikan Utilitas Media3 utama
Komponen UI (PlayerView, Compose UI), ViewModel, atau callback MediaSession Pengujian Robolectric lokal (src/test) atau pengujian UI berinstrumen (src/androidTest) FakePlayer
Integrasi ExoPlayer, transisi playlist, analisis, dan logika MediaSource atau DataSource kustom Pengujian Robolectric lokal (src/test) TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource
Decoding MediaCodec, rendering Surface, output audio, dan DRM hardware nyata Uji instrumentasi di perangkat fisik (src/androidTest) ExoPlayer.Builder, Player.await* ekstensi, PlayerFence

Jika memungkinkan, jalankan pengujian pemutaran instrumentasi di perangkat fisik, bukan emulator, karena implementasi stack media emulator bervariasi. Untuk mengetahui informasi selengkapnya tentang dukungan emulator, lihat panduan perangkat yang didukung.

Menambahkan dependensi pengujian

Tambahkan artefak pengujian Media3 ke blok dependencies file build.gradle.kts atau build.gradle modul Anda menggunakan versi yang sama dengan modul Media3 lainnya:

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"

Kedua modul ini membagi tanggung jawab berdasarkan lingkungan runtime:

Pastikan modul Anda juga menyertakan dependensi framework pengujian untuk lingkungan pengujian Anda:

  • Robolectric: org.robolectric:robolectric. Tetapkan android.testOptions.unitTests.isIncludeAndroidResources = true saat memuat aset lokal.
  • AndroidX Test: androidx.test:runner, androidx.test:core, dan androidx.test.ext:junit.
  • Coroutine Kotlin di thread utama: org.jetbrains.kotlinx:kotlinx-coroutines-android.

Ikut serta dalam API pengujian yang tidak stabil

Class dan metode di media3-test-utils dan media3-test-utils-robolectric dianotasikan dengan @UnstableApi. Meskipun Media3 bertujuan untuk menjaga helper pengujian yang banyak digunakan tetap ergonomis di seluruh rilis, utilitas pengujian tidak mengikuti aturan stabilitas kompatibilitas biner yang sama dengan API pemutaran produksi.

Dalam set sumber pengujian, anotasikan class atau metode pengujian dengan @androidx.annotation.OptIn, tambahkan keikutsertaan package-info untuk paket pengujian, atau konfigurasikan lint.xml untuk sumber pengujian. Untuk mengetahui informasi selengkapnya, lihat catatan pemecahan masalah lint API yang tidak stabil.

Pengujian lokal dengan Robolectric

Uji coba Robolectric lokal berjalan di JVM mesin pengembangan Anda tanpa perangkat fisik atau emulator. Robolectric tidak menyediakan implementasi MediaCodec, dan membuat Looper utama dijeda hingga dipompa secara eksplisit, sehingga instance ExoPlayer default tidak dapat mendekode media atau memberikan update status sendiri.

Membangun pemutar pengujian dan pemutaran lanjutan

Gunakan TestExoPlayerBuilder untuk membuat ExoPlayer yang dikonfigurasi untuk pengujian Robolectric. Secara default, TestExoPlayerBuilder mengonfigurasi komponen berikut:

  • FakeClock (isAutoAdvancing = true) yang maju otomatis yang mengoordinasikan progres waktu antara thread pemutaran latar belakang ExoPlayer dan Looper utama Robolectric yang dijeda.
  • Perender video dan audio palsu (FakeVideoRenderer dan FakeAudioRenderer) yang menggunakan sampel media sesuai dengan stempel waktunya tanpa memerlukan dekoder MediaCodec.

Untuk melanjutkan pemutaran dan menegaskan hasil di thread utama (dengan pemutar yang menggunakan Looper.getMainLooper, default di Robolectric), gunakan metode advance(player) atau play(player) statis di TestPlayerRunHelper (play(player) memanggil player.play sebelum menunggu). Untuk sebagian besar kondisi, helper akan memompa Looper utama Robolectric hingga kondisi target terpenuhi dan menampilkan pengecualian jika terjadi error pemutaran fatal atau non-fatal:

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

Dalam contoh ini, asset:///media/mp4/sample.mp4 merujuk pada contoh file media yang ditempatkan di direktori src/test/assets/ (atau src/main/assets/) modul Anda.

Saat meneruskan referensi ExoPlayer, advance(player) dan play(player) menampilkan ExoPlayerRunResult (atau PlayerRunResult saat meneruskan Player). Jenis hasil ini menyediakan metode untuk menunggu status pemutaran, posisi, transisi playlist, frame yang dirender, perintah yang tertunda, dan error pemutaran yang diharapkan, atau mengabaikan error non-fatal. Untuk mengetahui informasi selengkapnya tentang kondisi tunggu, lihat referensi API TestPlayerRunHelper.

Menguji dengan sumber palsu dan codec simulasi

Banyak pengujian integrasi memverifikasi navigasi playlist, pemilihan trek, pembaruan linimasa, atau perilaku percobaan ulang dan tidak memerlukan file media sungguhan. Anda dapat memasukkan linimasa dan data sintetis ke dalam pemutar TestExoPlayerBuilder menggunakan FakeMediaSource, FakeTimeline, FakeDataSource, atau WebServerDispatcher (dengan 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 menggunakan perender palsu secara default. Jika pengujian Robolectric Anda memerlukan interaksi MediaCodec, konfigurasikan codec yang disimulasikan dan pengambilan output sebagai berikut:

  1. Tambahkan withAllDefaultSupportedCodecs dari ShadowMediaCodecConfig sebagai @Rule JUnit untuk mendaftarkan codec simulasi Robolectric.
  2. Teruskan CapturingRenderersFactory yang dibuat dengan Clock (CapturingRenderersFactory(context, builder.getClock())) builder ke builder.setRenderersFactory.
  3. Saat menguji trek video, lampirkan Surface (seperti Surface(SurfaceTexture(1))) dengan player.setVideoSurface agar MediaCodecVideoRenderer merender frame yang didekode, bukan melewatinya.
  4. Untuk memverifikasi output yang dirender, panggil register(player, capturingRenderersFactory) di PlaybackOutput sebelum memulai pemutaran.
  5. Setelah pemutaran selesai, rilis pemutar dan platform, lalu periksa output yang diformat dari Dumper (Dumper().add(playbackOutput).toString()).

Uji instrumentasi di perangkat

Pengujian Robolectric lokal tidak menjalankan dekoder platform, trek audio, sesi DRM, atau rendering Surface yang sebenarnya. Untuk memverifikasi perilaku pemutaran menyeluruh di perangkat yang menjalankan Android, tulis uji instrumentasi di src/androidTest menggunakan ExoPlayer sungguhan yang dibuat dengan ExoPlayer.Builder.

Dalam uji instrumentasi JUnit, metode @Test berjalan di thread instrumentasi, bukan di thread utama aplikasi. Selalu buat, kontrol, dan rilis ExoPlayer di thread Looper aplikasinya, misalnya dengan menggunakan runBlocking(Dispatchers.Main) di Kotlin atau InstrumentationRegistry.getInstrumentation().runOnMainSync di Java.

Menunggu status pemain dengan PlayerFence dan coroutine

Dalam uji instrumentasi, Looper utama berjalan secara real time di perangkat. Jangan sertakan Robolectric atau media3-test-utils-robolectric dalam dependensi androidTestImplementation. Untuk menunggu status dan peristiwa pemutaran, media3-test-utils menyediakan helper berikut:

Kedua API mengawasi pemain saat menunggu. Jika terjadi error pemutaran fatal (atau, secara default, error nonfatal), atau jika waktu tunggu melebihi batas waktu (secara default 10 detik, atau 30 detik di emulator), panggilan akan langsung gagal, bukan hang:

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

Saat mem-build ExoPlayer dengan ApplicationProvider.getApplicationContext dalam modul aplikasi (com.android.application), URI asset:/// di-resolve terhadap aset aplikasi target (seperti src/debug/assets/ atau src/main/assets/, bukan src/androidTest/assets/).

Peristiwa sementara dan pengawasan kustom

Selain memeriksa status pemain saat ini (tempat tunggu langsung ditampilkan jika kondisi sudah terpenuhi), helper di media3-test-utils mendukung peristiwa satu kali dan kondisi pemroses kustom:

  • Peristiwa sementara seperti frame pertama yang dirender: Lampirkan output video (seperti Surface atau PlayerView) ke pemutar, dan mulai menunggu dengan awaitFirstFrameRendered atau rendersFirstFrame sebelum memanggil player.prepare agar peristiwa frame satu kali awal tidak terlewatkan.
  • Kondisi kustom: Gabungkan pemroses kustom atau tunggu coroutine di dalam awaitSupervised di player di Kotlin, atau teruskan ListenableFuture kustom ke futureWhen(player).withSupervision(future) di Java. Saat menguji pemulihan dari error tidak fatal, teruskan failOnNonFatalErrors = false (Kotlin) atau panggil ignoringNonFatalErrors (Java), dan sesuaikan waktu tunggu dengan timeout atau withTimeoutMs(timeoutMs).

Contoh berikut menunggu peristiwa frame pertama sementara dan mengawasi pemroses metadata kustom:

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

Menguji UI dan komponen aplikasi dengan FakePlayer

Saat menguji komponen UI seperti composable PlayerView atau media3-ui-compose, ViewModel, atau callback MediaSession, membuat ExoPlayer lengkap sering kali tidak diperlukan. FakePlayer di media3-test-utils memperluas SimpleBasePlayer untuk menyimulasikan playlist, status pemutaran, posisi, ukuran video, isyarat, dan error pada Looper thread panggilan. Looper ini tersedia secara otomatis dalam pengujian Robolectric dengan @RunWith(AndroidJUnit4::class) dan di thread utama dalam uji instrumentasi.

Secara default, FakePlayer menyimulasikan penundaan buffering 100 md pada prepare dan seekTo; teruskan bufferingDelayMs = 0 di Kotlin atau tetapkan status secara langsung dengan 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();

Utilitas pengujian tambahan

media3-test-utils menyertakan helper khusus untuk skenario pengujian lainnya:

  • Tombol fitur dan mode Canary: Gunakan Media3FlagsRule bersama dengan @BindFlag dan @BindCanaryMode untuk mereset penggantian flag setelah setiap pengujian dan menjalankan pengujian berparameter di seluruh status flag. Untuk mengetahui informasi selengkapnya, lihat pengujian dengan tombol fitur.
  • Pengujian kontrak DataSource kustom: Buat subclass DataSourceContractTest untuk memverifikasi bahwa penerapan DataSource atau HttpDataSource kustom sesuai dengan ekspektasi kontrak Media3 di seluruh permintaan rentang, penanganan error, dan pemroses transfer. Untuk mengetahui informasi selengkapnya, lihat mengintegrasikan stack jaringan lainnya.