Тестирование

Media3 предоставляет специальные артефакты тестирования (media3-test-utils и media3-test-utils-robolectric), которые помогают тестировать воспроизведение медиа, переходы между состояниями проигрывателя, компоненты пользовательского интерфейса и пользовательские источники медиа как в локальных тестах JVM, так и в инструментальных тестах устройства.

На этой странице описаны рекомендуемые методы тестирования приложений, использующих ExoPlayer, и показано, как использовать утилиты тестирования Media3 в каждой тестовой среде.

Рекомендации по тестированию

При написании автоматических тестов для воспроизведения медиаконтента следуйте следующим основным принципам:

  • Используйте реальные компоненты или поддельные компоненты Media3 вместо моков. Не создавайте моки Player или ExoPlayer с помощью фреймворков, таких как Mockito. Player имеет инварианты состояния и контракты слушателей, которые не применяются макетами, что может сделать тесты хрупкими или скрыть ошибки. Вместо этого используйте реальный экземпляр ExoPlayer, FakePlayer или подделки, такие как FakeMediaSource и FakeDataSource. Используйте макеты для проверки вызовов пользовательских обратных вызовов, когда это необходимо.
  • Используйте детерминированное ожидание вместо приостановки. ExoPlayer загружает и воспроизводит медиаконтент асинхронно во внутреннем потоке. Не используйте произвольные вызовы Thread.sleep, так как они замедляют тесты и делают их нестабильными. Используйте TestPlayerRunHelper в тестах Robolectric и приостанавливайте расширения Player или PlayerFence в инструментальных тестах. Эти вспомогательные методы ожидают явных условий проигрывателя и сразу же завершаются с ошибкой, если происходит ошибка воспроизведения.
  • Обращайтесь к проигрывателю в потоке приложения. Экземпляры ExoPlayer и вспомогательные элементы ожидания должны вызываться в потоке Looper приложения проигрывателя (обычно это основной поток). Дополнительную информацию можно найти в требованиях к потокам ExoPlayer.

Как выбрать подходящую тестовую среду

Для разных уровней интеграции медиаконтента подходят разные тестовые среды:

Цель тестирования Рекомендуемая среда Основные утилиты Media3
Компоненты пользовательского интерфейса (PlayerView, Compose UI), ViewModels или обратные вызовы MediaSession. Локальные тесты Robolectric (src/test) или инструментальные тесты интерфейса (src/androidTest). FakePlayer
Интеграция с ExoPlayer, переходы между плейлистами, аналитика и настраиваемая логика MediaSource или DataSource Локальные тесты Robolectric (src/test) TestExoPlayerBuilder, TestPlayerRunHelper, FakeMediaSource
Реальное аппаратное декодирование MediaCodec, рендеринг Surface, вывод аудио и DRM Инструментальные тесты на физических устройствах (src/androidTest) ExoPlayer.Builder, Player.await* расширений, PlayerFence

По возможности запускайте инструментальные тесты воспроизведения на физических устройствах, а не на эмуляторах, поскольку реализации медиастека эмулятора различаются. Дополнительную информацию о поддержке эмуляторов можно найти в руководстве по поддерживаемым устройствам.

Добавьте зависимости для тестирования

Добавьте тестовые артефакты Media3 в блок dependencies файла build.gradle.kts или build.gradle вашего модуля, используя ту же версию, что и в других модулях 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")

Яркий

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"

Обязанности двух модулей разделены по среде выполнения:

Убедитесь, что в модуль также включены зависимости тестового фреймворка для тестовых сред:

  • Robolectric: org.robolectric:robolectric. При загрузке локальных объектов задайте для android.testOptions.unitTests.isIncludeAndroidResources = true значение 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. Хотя мы стремимся сделать так, чтобы часто используемые вспомогательные инструменты для тестирования были удобны в разных выпусках, на утилиты для тестирования не распространяются те же правила стабильности двоичной совместимости, что и на API воспроизведения.

В наборах исходных файлов для тестирования добавьте к тестовым классам или методам аннотацию @androidx.annotation.OptIn, включите package-info для тестовых пакетов или настройте lint.xml для тестовых исходных файлов. Подробную информацию вы найдете в примечаниях по устранению неполадок с нестабильным API.

Локальные тесты с помощью Robolectric

Локальные тесты Robolectric выполняются на виртуальной машине Java компьютера для разработки без физического устройства или эмулятора. Robolectric не предоставляет реализации MediaCodec и приостанавливает основной Looper, пока он не будет явно запущен, поэтому экземпляр ExoPlayer по умолчанию не может самостоятельно декодировать медиаконтент или предоставлять обновления статуса.

Как создать тестовый проигрыватель и настроить воспроизведение

Используйте TestExoPlayerBuilder, чтобы создать ExoPlayer, настроенный для тестов Robolectric. По умолчанию TestExoPlayerBuilder настраивает следующие компоненты:

  • Автоматически продвигающийся FakeClock (isAutoAdvancing = true), который координирует ход времени между фоновым потоком воспроизведения ExoPlayer и приостановленным основным потоком Looper Robolectric.
  • Имитаторы видео- и аудиорендереров (FakeVideoRenderer и FakeAudioRenderer), которые используют медиафрагменты в соответствии с их временными метками без декодеров MediaCodec.

Чтобы продвинуть воспроизведение и подтвердить результаты в основном потоке (с проигрывателем, использующим Looper.getMainLooper, по умолчанию в Robolectric), используйте статические методы advance(player) или play(player) в TestPlayerRunHelper (play(player) вызывает player.play перед ожиданием). В большинстве случаев вспомогательный метод запускает основной метод Looper Robolectric, пока не будет выполнено целевое условие. Если возникает критическая или некритическая ошибка воспроизведения, генерируется исключение.

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 (или PlayerRunResult при передаче Player). Эти типы результатов предоставляют методы для ожидания состояний воспроизведения, позиций, переходов между плейлистами, отрисованных кадров, ожидающих команд и ожидаемых ошибок воспроизведения или игнорирования некритических ошибок. Дополнительную информацию об условиях ожидания можно найти в документации по API TestPlayerRunHelper.

Тестирование с поддельными источниками и симулированными кодеками

Многие интеграционные тесты проверяют навигацию по плейлисту, выбор трека, обновление временной шкалы или поведение при повторной попытке и не требуют реальных медиафайлов. Вы можете передавать синтетические временные шкалы и данные в проигрыватель TestExoPlayerBuilder с помощью FakeMediaSource, FakeTimeline, FakeDataSource или WebServerDispatcher (с 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 использует поддельные средства визуализации. Если в тесте Robolectric нужно использовать взаимодействия MediaCodec, настройте симулированные кодеки и запись выходных данных следующим образом:

  1. Добавьте withAllDefaultSupportedCodecs из ShadowMediaCodecConfig в качестве JUnit @Rule, чтобы зарегистрировать симулированные кодеки Robolectric.
  2. Передайте объект CapturingRenderersFactory, созданный с помощью метода Clock (CapturingRenderersFactory(context, builder.getClock())) конструктора, в метод builder.setRenderersFactory.
  3. При тестировании видеодорожек прикрепите Surface (например, Surface(SurfaceTexture(1))) с player.setVideoSurface, чтобы MediaCodecVideoRenderer отображал декодированные кадры, а не пропускал их.
  4. Чтобы проверить обработанные выходные данные, вызовите register(player, capturingRenderersFactory) в PlaybackOutput перед началом воспроизведения.
  5. После завершения воспроизведения освободите проигрыватель и поверхность, а затем проверьте отформатированные выходные данные из Dumper (Dumper().add(playbackOutput).toString()).

Инструментальные тесты на устройствах

Локальные тесты Robolectric не выполняют реальные декодеры платформы, аудиодорожки, сеансы DRM или рендеринг Surface. Чтобы проверить сквозное воспроизведение на устройствах Android, напишите инструментальные тесты в src/androidTest, используя реальный экземпляр ExoPlayer, созданный с помощью ExoPlayer.Builder.

В инструментальных тестах JUnit методы @Test выполняются в потоке инструмента, а не в основном потоке приложения. Всегда создавайте, контролируйте и освобождайте ExoPlayer в потоке приложения Looper, например с помощью runBlocking(Dispatchers.Main) в Kotlin или InstrumentationRegistry.getInstrumentation().runOnMainSync в Java.

Как отслеживать состояния игроков с помощью PlayerFence и сопрограмм

В инструментальных тестах основной Looper выполняется на устройстве в реальном времени. Не включайте Robolectric или media3-test-utils-robolectric в зависимости androidTestImplementation. Чтобы дождаться состояний и событий воспроизведения, в media3-test-utils предусмотрены следующие вспомогательные средства:

  • Kotlin. Приостановка функций расширения в Player (например, awaitPlaybackState, awaitIsPlaying, awaitFirstFrameRendered и awaitContentPositionAtLeast).
  • Java. Статический метод futureWhen(player) в PlayerFence, который возвращает PlayerFence. Методы условий (например, entersPlaybackState и isPlaying) должны вызываться в потоке Looper проигрывателя и возвращать ListenableFuture, который тестовый поток может заблокировать с помощью get.

Оба 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));
  }
}

При сборке ExoPlayer с помощью ApplicationProvider.getApplicationContext в модуле приложения (com.android.application) URI asset:/// разрешаются в отношении объектов целевого приложения (например, src/debug/assets/ или src/main/assets/, а не src/androidTest/assets/).

Временные события и настраиваемый контроль

Помимо проверки текущего состояния проигрывателя (когда ожидание завершается немедленно, если условие уже выполнено), вспомогательные функции в media3-test-utils поддерживают однократные события и пользовательские условия прослушивателя:

  • Временные события, например первый отрисованный кадр. Подключите к проигрывателю вывод видео (например, Surface или PlayerView) и начните ожидание с помощью awaitFirstFrameRendered или rendersFirstFrame, прежде чем вызывать player.prepare, чтобы не пропустить раннее однократное событие кадра.
  • Специальные условия. Оберните специальный прослушиватель или сопрограмму в awaitSupervised в player в Kotlin или передайте специальный ListenableFuture в futureWhen(player).withSupervision(future) в Java. При тестировании восстановления после устранимых ошибок передайте 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

При тестировании компонентов интерфейса, таких как composable-функции PlayerView или media3-ui-compose, ViewModel или обратные вызовы MediaSession, создавать полный экземпляр ExoPlayer часто не требуется. FakePlayer в media3-test-utils расширяет SimpleBasePlayer, чтобы симулировать плейлисты, состояния воспроизведения, позиции, размеры видео, подсказки и ошибки в Looper вызывающего потока. Этот Looper автоматически доступен в тестах Robolectric с @RunWith(AndroidJUnit4::class) и в основном потоке в инструментальных тестах.

По умолчанию FakePlayer имитирует задержку буферизации в 100 мс для prepare и seekTo. Передайте bufferingDelayMs = 0 в Kotlin или задайте состояния напрямую с помощью 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 включает специализированные помощники для других сценариев тестирования: