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"
Обязанности двух модулей разделены по среде выполнения:
media3-test-utils– кросс-платформенные утилиты для локальных и инструментальных тестов, включаяTestExoPlayerBuilder,PlayerFence(и расширения KotlinPlayer.await*),FakePlayer,FakeClock,FakeMediaSource,FakeDataSource,DataSourceContractTestиMedia3FlagsRule.media3-test-utils-robolectric: утилиты для тестов Robolectric на локальной виртуальной машине Java, включаяTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryиPlaybackOutput.
Убедитесь, что в модуль также включены зависимости тестового фреймворка для тестовых сред:
- 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и приостановленным основным потокомLooperRobolectric. - Имитаторы видео- и аудиорендереров (
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, настройте симулированные кодеки и запись выходных данных следующим образом:
- Добавьте
withAllDefaultSupportedCodecsизShadowMediaCodecConfigв качестве JUnit@Rule, чтобы зарегистрировать симулированные кодеки Robolectric. - Передайте объект
CapturingRenderersFactory, созданный с помощью методаClock(CapturingRenderersFactory(context, builder.getClock())) конструктора, в методbuilder.setRenderersFactory. - При тестировании видеодорожек прикрепите
Surface(например,Surface(SurfaceTexture(1))) сplayer.setVideoSurface, чтобыMediaCodecVideoRendererотображал декодированные кадры, а не пропускал их. - Чтобы проверить обработанные выходные данные, вызовите
register(player, capturingRenderersFactory)вPlaybackOutputперед началом воспроизведения. - После завершения воспроизведения освободите проигрыватель и поверхность, а затем проверьте отформатированные выходные данные из
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 включает специализированные помощники для других сценариев тестирования:
- Флаги функций и режим Canary. Используйте
Media3FlagsRuleвместе с@BindFlagи@BindCanaryMode, чтобы сбрасывать переопределения флагов после каждого теста и запускать параметризованные тесты для разных состояний флагов. Подробнее о тестировании с помощью флагов функций… - Специальные тесты контрактов
DataSource. ПодклассDataSourceContractTest, который проверяет, соответствуют ли специальные реализацииDataSourceилиHttpDataSourceтребованиям контракта Media3 в отношении запросов диапазона, обработки ошибок и прослушивателей передачи. Подробнее об интеграции других сетевых стеков…