Media3 提供專屬測試構件 (media3-test-utils 和 media3-test-utils-robolectric),協助您在本機 JVM 測試和裝置檢測設備測試中,測試媒體播放、播放器狀態轉換、UI 元件和自訂媒體來源。
本頁面概述測試使用 ExoPlayer 的應用程式時,建議採取的做法,並說明如何在各個測試環境中使用 Media3 的測試公用程式。
測試建議
編寫媒體播放的自動化測試時,請遵循下列核心原則:
- 建議使用實際元件或 Media3 假元件,而非模擬元件:請避免使用 Mockito 等架構模擬
Player或ExoPlayer。Player具有狀態不變量和事件監聽器合約,模擬物件不會強制執行這些合約,這可能會導致測試不穩定或隱藏錯誤。請改用實際的ExoPlayer執行個體、FakePlayer,或FakeMediaSource和FakeDataSource等假資料。視需要保留模擬物件,用於驗證自訂回呼叫用。 - 以確定性等待取代休眠:
ExoPlayer會在內部執行緒上,以非同步方式載入及播放媒體。避免任意呼叫Thread.sleep,以免測試速度變慢且不穩定。在 Robolectric 測試中使用TestPlayerRunHelper,並在檢測設備測試中暫停Player擴充功能或PlayerFence。這些輔助程式會等待明確的播放器條件,並在發生播放錯誤時立即失敗。 - 在應用程式執行緒上存取播放器:必須在播放器的應用程式
Looper執行緒 (通常是主執行緒) 上呼叫ExoPlayer執行個體和等待輔助程式。詳情請參閱 ExoPlayer 執行緒需求。
選擇合適的測試環境
媒體整合的不同層級適用於不同的測試環境:
| 測試目標 | 建議環境 | 重要 Media3 公用程式 |
|---|---|---|
UI 元件 (PlayerView、Compose UI)、ViewModel 或 MediaSession 回呼 |
本機 Robolectric 測試 (src/test) 或檢測設備 UI 測試 (src/androidTest) |
FakePlayer |
ExoPlayer 整合、播放清單轉換、數據分析,以及自訂 MediaSource 或 DataSource 邏輯 |
本機 Robolectric 測試 (src/test) |
TestExoPlayerBuilder、TestPlayerRunHelper、FakeMediaSource |
實體硬體 MediaCodec 解碼、Surface 轉譯、音訊輸出和 DRM |
在實體裝置上進行檢測設備測試 (src/androidTest) |
ExoPlayer.Builder,Player.await* 擴充功能,PlayerFence |
請盡可能在實體裝置上執行檢測設備播放測試,而非模擬器,因為模擬器媒體堆疊實作方式不盡相同。如要進一步瞭解模擬器支援,請參閱支援的裝置指南。
新增測試依附元件
將 Media3 測試構件新增至模組 build.gradle.kts 或 build.gradle 檔案的 dependencies 區塊,並使用與其他 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")
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"
這兩個模組會依執行階段環境劃分責任:
media3-test-utils:本機和檢測設備測試的跨環境公用程式,包括TestExoPlayerBuilder、PlayerFence(和 KotlinPlayer.await*擴充功能)、FakePlayer、FakeClock、FakeMediaSource、FakeDataSource、DataSourceContractTest和Media3FlagsRule。media3-test-utils-robolectric:本機 JVM 上 Robolectric 測試的公用程式,包括TestPlayerRunHelper、RobolectricUtil、ShadowMediaCodecConfig、CapturingRenderersFactory和PlaybackOutput。
請確認模組也包含測試環境的測試架構依附元件:
- Robolectric:
org.robolectric:robolectric。載入本機素材資源時,請設定android.testOptions.unitTests.isIncludeAndroidResources = true。 - AndroidX Test:
androidx.test:runner、androidx.test:core和androidx.test.ext:junit。 - 主執行緒上的 Kotlin 協同程式:
org.jetbrains.kotlinx:kotlinx-coroutines-android。
選擇加入不穩定的測試 API
media3-test-utils 和 media3-test-utils-robolectric 中的類別和方法會加上 @UnstableApi 註解。雖然 Media3 的目標是讓廣泛使用的測試輔助程式在各版本中保持易用性,但測試公用程式不遵循與正式版播放 API 相同的二進位相容性穩定性規則。
在測試來源集中,使用 @androidx.annotation.OptIn 註解測試類別或方法、為測試套件新增 package-info 選擇加入,或為測試來源設定 lint.xml。詳情請參閱不穩定 API Lint 疑難排解附註。
使用 Robolectric 進行本機測試
本機 Robolectric 測試會在開發機器的 JVM 上執行,不需要實體裝置或模擬器。Robolectric 不提供 MediaCodec 實作,且會將主要 Looper 保持暫停狀態,直到明確觸發為止,因此預設的 ExoPlayer 執行個體無法自行解碼媒體或提供狀態更新。
建構測試播放器並推進播放
使用 TestExoPlayerBuilder 建立為 Robolectric 測試設定的 ExoPlayer。根據預設,TestExoPlayerBuilder 會設定下列元件:
- 自動推進的
FakeClock(isAutoAdvancing = true),可協調ExoPlayer的背景播放執行緒與 Robolectric 暫停的主要Looper之間的時間進度。 - 假影片和音訊轉譯器 (
FakeVideoRenderer和FakeAudioRenderer) 會根據時間戳記使用媒體樣本,不需要MediaCodec解碼器。
如要在主執行緒上推進播放並斷言結果 (播放器使用 Looper.getMainLooper,Robolectric 中的預設值),請在 TestPlayerRunHelper 上使用靜態 advance(player) 或 play(player) 方法 (play(player) 會在等待前呼叫 player.play)。在大多數情況下,輔助程式會推送 Robolectric 的主要 Looper,直到符合目標條件為止。如果發生嚴重或非嚴重播放錯誤,則會擲回例外狀況:
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); } }
在本例中,asset:///media/mp4/sample.mp4 是指放置在模組 src/test/assets/ (或 src/main/assets/) 目錄中的範例媒體檔案。
傳遞 ExoPlayer 參照時,advance(player) 和 play(player) 會傳回 ExoPlayerRunResult (或傳遞 Player 時傳回 PlayerRunResult)。這些結果類型提供的方法可用於等待播放狀態、位置、播放清單轉換、算繪影格、待處理指令和預期播放錯誤,或忽略非嚴重錯誤。如要進一步瞭解等待條件,請參閱 TestPlayerRunHelper API 參考資料。
使用假來源和模擬轉碼器進行測試
許多整合測試會驗證播放清單導覽、曲目選取、時間軸更新或重試行為,不需要實際的媒體檔案。您可以使用 FakeMediaSource、FakeTimeline、FakeDataSource 或 WebServerDispatcher (搭配 OkHttp MockWebServer),將合成時間軸和資料饋送至 TestExoPlayerBuilder 播放器:
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 預設會使用假轉譯器。如果 Robolectric 測試需要 MediaCodec 互動,請按照下列方式設定模擬轉碼器和輸出擷取:
- 從
ShadowMediaCodecConfig新增withAllDefaultSupportedCodecs做為 JUnit@Rule,以註冊 Robolectric 的模擬轉碼器。 - 將使用建構工具的
Clock(CapturingRenderersFactory(context, builder.getClock())) 建構的CapturingRenderersFactory傳遞至builder.setRenderersFactory。 - 測試影片軌時,請使用
player.setVideoSurface附加Surface(例如Surface(SurfaceTexture(1))),讓MediaCodecVideoRenderer轉譯解碼的影格,而不是略過。 - 如要驗證算繪輸出內容,請在開始播放前,對
PlaybackOutput呼叫register(player, capturingRenderersFactory)。 - 播放結束後,釋出播放器和介面,然後檢查
Dumper(Dumper().add(playbackOutput).toString()) 的格式化輸出內容。
在裝置上執行檢測設備測試
本機 Robolectric 測試不會執行實際的平台解碼器、音軌、DRM 工作階段或 Surface 轉譯。如要驗證搭載 Android 系統的裝置上的端對端播放行為,請使用以 ExoPlayer.Builder 建構的實際 ExoPlayer,在 src/androidTest 中編寫檢測設備測試。
在 JUnit 檢測設備測試中,@Test 方法會在檢測設備執行緒上執行,而非應用程式的主執行緒。請一律在應用程式的 Looper 執行緒上建立、控制及釋出 ExoPlayer,例如在 Kotlin 中使用 runBlocking(Dispatchers.Main),或在 Java 中使用 InstrumentationRegistry.getInstrumentation().runOnMainSync。
使用 PlayerFence 和協同程式等待播放器狀態
在檢測設備測試中,主要 Looper 會在裝置上即時執行。請勿在 androidTestImplementation 依附元件中加入 Robolectric 或 media3-test-utils-robolectric。如要等待播放狀態和事件,media3-test-utils 提供下列輔助程式:
- Kotlin:暫停
Player上的擴充功能函式 (例如awaitPlaybackState、awaitIsPlaying、awaitFirstFrameRendered和awaitContentPositionAtLeast)。 - Java:
PlayerFence上的靜態futureWhen(player)方法,會傳回PlayerFence,其條件方法 (例如entersPlaybackState和isPlaying) 必須在播放器的Looper執行緒上呼叫,並傳回測試執行緒可使用get封鎖的ListenableFuture。
這兩個 API 都會在等待期間監控播放器。如果發生重大播放錯誤 (或預設為非重大錯誤),或等待時間超過逾時時間 (預設為 10 秒,模擬器則為 30 秒),呼叫會立即失敗,而不會停止回應:
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)); } }
在應用程式模組 (com.android.application) 中使用 ApplicationProvider.getApplicationContext 建構 ExoPlayer 時,asset:/// URI 會解析目標應用程式的資產 (例如 src/debug/assets/ 或 src/main/assets/,而非 src/androidTest/assets/)。
暫時性事件和自訂監控
除了檢查目前的播放器狀態 (如果已符合條件,等待會立即傳回) 之外,media3-test-utils 中的輔助程式也支援一次性事件和自訂事件監聽器條件:
- 暫時性事件,例如第一個算繪影格:將影片輸出 (例如
Surface或PlayerView) 附加至播放器,並在呼叫player.prepare之前使用awaitFirstFrameRendered或rendersFirstFrame開始等待,以免錯過早期的單次影格事件。 - 自訂條件:在 Kotlin 中,將自訂事件監聽器或協同程式等待包裝在
player的awaitSupervised內,或在 Java 中將自訂ListenableFuture傳遞至futureWhen(player).withSupervision(future)。測試從非致命錯誤復原時,請傳遞failOnNonFatalErrors = false(Kotlin) 或呼叫ignoringNonFatalErrors(Java),並使用timeout或withTimeoutMs(timeoutMs)自訂逾時。
以下範例會等待暫時性的第一影格事件,並監控自訂中繼資料事件監聽器:
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 測試 UI 和應用程式元件
測試 PlayerView 或 media3-ui-compose 可組合項、ViewModel 或 MediaSession 回呼等 UI 元件時,通常不需要建立完整的 ExoPlayer。media3-test-utils 中的 FakePlayer 擴充 SimpleBasePlayer,在呼叫執行緒的 Looper 上模擬播放清單、播放狀態、位置、影片大小、提示和錯誤。這個 Looper 會自動在 Robolectric 測試中與 @RunWith(AndroidJUnit4::class) 搭配使用,並在檢測設備測試的主執行緒中提供。
根據預設,FakePlayer 會在 prepare 和 seekTo 上模擬 100 毫秒的緩衝延遲時間;在 Kotlin 中傳遞 bufferingDelayMs = 0,或直接使用 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();
其他測試公用程式
media3-test-utils 包含其他測試情境的專用輔助程式:
- 功能旗標和 Canary 模式:搭配使用
Media3FlagsRule、@BindFlag和@BindCanaryMode,在每次測試後重設旗標覆寫,並在不同旗標狀態下執行參數化測試。詳情請參閱「使用功能旗標進行測試」。 - 自訂
DataSource合約測試:子類別DataSourceContractTest,用於驗證自訂DataSource或HttpDataSource實作是否符合 Media3 的合約預期,包括範圍要求、錯誤處理和傳輸事件監聽器。詳情請參閱「整合其他網路堆疊」。