テスト

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 つのモジュールは、ランタイム環境ごとに責任を分担します。

モジュールに、テスト環境のテスト フレームワークの依存関係も含まれていることを確認します。

  • 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 の操作が必要な場合は、次のようにシミュレートされたコーデックと出力キャプチャを構成します。

  1. ShadowMediaCodecConfig から withAllDefaultSupportedCodecs を JUnit @Rule として追加し、Robolectric のシミュレートされたコーデックを登録します。
  2. ビルダーの Clock(CapturingRenderersFactory(context, builder.getClock()))でビルドされた CapturingRenderersFactory を builder.setRenderersFactory に渡します。
  3. 動画トラックをテストする場合は、player.setVideoSurface を使用して Surface(Surface(SurfaceTexture(1)) など)をアタッチします。これにより、MediaCodecVideoRenderer はデコードされたフレームをスキップせずにレンダリングします。
  4. レンダリングされた出力を検証するには、再生を開始する前に PlaybackOutput で register(player, capturingRenderersFactory) を呼び出します。
  5. 再生が終了したら、プレーヤーとサーフェスを解放し、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 の契約要件に準拠していることを確認します。詳細については、他のネットワーク スタックの統合をご覧ください。