Media3 には、メディア再生、プレーヤーの状態遷移、UI コンポーネント、カスタム メディアソースをローカル JVM テストとデバイス インストルメンテーション テストの両方でテストするための専用のテスト アーティファクト(media3-test-utils と media3-test-utils-robolectric)が用意されています。
このページでは、ExoPlayer を使用するアプリをテストする際のおすすめの方法と、各テスト環境で Media3 のテスト ユーティリティを使用する方法について説明します。
テストに関する推奨事項
メディア再生の自動テストを作成する際は、次の基本原則に従ってください。
- モックよりも実際のコンポーネントまたは Media3 のフェイクを使用する: Mockito などのフレームワークで
PlayerまたはExoPlayerをモックすることは避けてください。Playerには、モックが適用しない状態不変条件とリスナー コントラクトがあります。これにより、テストが脆弱になったり、バグが隠れたりする可能性があります。代わりに、実際のExoPlayerインスタンス、FakePlayer、またはFakeMediaSourceやFakeDataSourceなどのフェイクを使用します。モックは、必要に応じてカスタム コールバックの呼び出しを検証するために予約します。 - スリープではなく、決定論的に待機する:
ExoPlayerは、内部スレッドでメディアの読み込みと再生を非同期的に行います。任意のThread.sleep呼び出しは避けてください。テストが遅くなり、不安定になります。Robolectric テストではTestPlayerRunHelperを使用し、インストゥルメンテーション テストではPlayer拡張機能またはPlayerFenceを一時停止します。これらのヘルパーは、明示的なプレーヤー条件を待機し、再生エラーが発生するとすぐに失敗します。 - アプリケーション スレッドでプレーヤーにアクセスする:
ExoPlayerインスタンスと待機ヘルパーは、プレーヤーのアプリケーションLooperスレッド(通常はメインスレッド)で呼び出す必要があります。詳細については、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"
2 つのモジュールは、ランタイム環境ごとに責任を分担します。
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の間で時間の進行を調整します。 MediaCodecデコーダを必要とせずに、タイムスタンプに従ってメディア サンプルを消費する、疑似動画レンダラ(FakeVideoRenderer)と疑似音声レンダラ(FakeAudioRenderer)。
メインスレッドで再生を進めて結果をアサートするには(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 メソッドはアプリケーションのメインスレッドではなく、インストルメンテーション スレッドで実行されます。ExoPlayer の作成、制御、リリースは、常にアプリケーションの Looper スレッドで行います。たとえば、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/androidTest/assets/ ではなく src/debug/assets/ や src/main/assets/ など)に対して解決されます。
一時的なイベントとカスタム監視
media3-test-utils のヘルパーは、現在のプレーヤーの状態を確認する(条件がすでに満たされている場合は待機がすぐに返される)だけでなく、1 回限りのイベントとカスタム リスナー条件をサポートしています。
- 最初のレンダリング フレームなどの一時的なイベント: 動画出力(
SurfaceやPlayerViewなど)をプレーヤーにアタッチし、player.prepareを呼び出す前にawaitFirstFrameRenderedまたはrendersFirstFrameで待機を開始して、初期の 1 回限りのフレーム イベントを見逃さないようにします。 - カスタム条件: 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 は、@RunWith(AndroidJUnit4::class) を使用した Robolectric テストと、インストルメンテーション テストのメインスレッドで自動的に使用できます。
デフォルトでは、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 には、他のテストシナリオ用の専用ヘルパーが含まれています。
- フィーチャー トグルとカナリアモード:
Media3FlagsRuleを@BindFlagおよび@BindCanaryModeと組み合わせて使用すると、各テストの後にフラグのオーバーライドをリセットし、フラグの状態全体でパラメータ化されたテストを実行できます。詳細については、フィーチャー トグルを使用したテストをご覧ください。 - カスタム
DataSource契約テスト:DataSourceContractTestをサブクラス化して、カスタムDataSourceまたはHttpDataSourceの実装が、範囲リクエスト、エラー処理、転送リスナー全体で Media3 の契約要件に準拠していることを確認します。詳細については、他のネットワーク スタックの統合をご覧ください。