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
PlayeratauExoPlayerdengan framework seperti Mockito.Playermemiliki invarian status dan kontrak pemroses yang tidak diterapkan oleh tiruan, yang dapat membuat pengujian menjadi rapuh atau menyembunyikan bug. Sebagai gantinya, gunakan instanceExoPlayeryang sebenarnya,FakePlayer, atau palsu sepertiFakeMediaSourcedanFakeDataSource. Cadangkan tiruan untuk memverifikasi pemanggilan callback kustom jika diperlukan. - Menunggu secara deterministik, bukan tidur:
ExoPlayermelakukan pemuatan dan pemutaran media secara asinkron pada thread internal. Hindari panggilanThread.sleeparbitrer, yang membuat pengujian menjadi lambat dan tidak stabil. GunakanTestPlayerRunHelperdalam pengujian Robolectric dan menangguhkan ekstensiPlayeratauPlayerFencedalam uji instrumentasi. Helper ini menunggu kondisi pemutar yang eksplisit dan segera gagal jika terjadi error pemutaran. - Mengakses pemutar di thread aplikasinya: Instance
ExoPlayerdan helper tunggu harus dipanggil di threadLooperaplikasi 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:
media3-test-utils: Utilitas lintas lingkungan untuk pengujian lokal dan uji instrumentasi, termasukTestExoPlayerBuilder,PlayerFence(dan ekstensiPlayer.await*Kotlin),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTest, danMedia3FlagsRule.media3-test-utils-robolectric: Utilitas untuk pengujian Robolectric di JVM lokal, termasukTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactory, danPlaybackOutput.
Pastikan modul Anda juga menyertakan dependensi framework pengujian untuk lingkungan pengujian Anda:
- Robolectric:
org.robolectric:robolectric. Tetapkanandroid.testOptions.unitTests.isIncludeAndroidResources = truesaat memuat aset lokal. - AndroidX Test:
androidx.test:runner,androidx.test:core, danandroidx.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 belakangExoPlayerdanLooperutama Robolectric yang dijeda.- Perender video dan audio palsu (
FakeVideoRendererdanFakeAudioRenderer) yang menggunakan sampel media sesuai dengan stempel waktunya tanpa memerlukan dekoderMediaCodec.
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:
- Tambahkan
withAllDefaultSupportedCodecsdariShadowMediaCodecConfigsebagai@RuleJUnit untuk mendaftarkan codec simulasi Robolectric. - Teruskan
CapturingRenderersFactoryyang dibuat denganClock(CapturingRenderersFactory(context, builder.getClock())) builder kebuilder.setRenderersFactory. - Saat menguji trek video, lampirkan
Surface(sepertiSurface(SurfaceTexture(1))) denganplayer.setVideoSurfaceagarMediaCodecVideoRenderermerender frame yang didekode, bukan melewatinya. - Untuk memverifikasi output yang dirender, panggil
register(player, capturingRenderersFactory)diPlaybackOutputsebelum memulai pemutaran. - 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:
- Kotlin: Menangguhkan fungsi ekstensi di
Player(sepertiawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRendered, danawaitContentPositionAtLeast). - Java: Metode
futureWhen(player)statis diPlayerFence, yang menampilkanPlayerFenceyang metode kondisinya (sepertientersPlaybackStatedanisPlaying) harus dipanggil di threadLooperpemutar dan menampilkanListenableFutureyang dapat diblokir oleh thread pengujian denganget.
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
SurfaceatauPlayerView) ke pemutar, dan mulai menunggu denganawaitFirstFrameRenderedataurendersFirstFramesebelum memanggilplayer.prepareagar peristiwa frame satu kali awal tidak terlewatkan. - Kondisi kustom: Gabungkan pemroses kustom atau tunggu coroutine di dalam
awaitSuperviseddiplayerdi Kotlin, atau teruskanListenableFuturekustom kefutureWhen(player).withSupervision(future)di Java. Saat menguji pemulihan dari error tidak fatal, teruskanfailOnNonFatalErrors = false(Kotlin) atau panggilignoringNonFatalErrors(Java), dan sesuaikan waktu tunggu dengantimeoutatauwithTimeoutMs(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
Media3FlagsRulebersama dengan@BindFlagdan@BindCanaryModeuntuk 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
DataSourcekustom: Buat subclassDataSourceContractTestuntuk memverifikasi bahwa penerapanDataSourceatauHttpDataSourcekustom sesuai dengan ekspektasi kontrak Media3 di seluruh permintaan rentang, penanganan error, dan pemroses transfer. Untuk mengetahui informasi selengkapnya, lihat mengintegrasikan stack jaringan lainnya.