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של האפליקציה של הנגן (בדרך כלל השרשור הראשי). מידע נוסף זמין במאמר דרישות ה-threading של ExoPlayer.
בחירת סביבת הבדיקה המתאימה
לשכבות שונות של שילוב המדיה מתאימות סביבות בדיקה שונות:
| יעד הבדיקה | הסביבה המומלצת | כלי עזר עיקריים של Media3 |
|---|---|---|
רכיבי ממשק משתמש (PlayerView, ממשק משתמש של פיתוח נייטיב), ViewModels או קריאות חוזרות (callbacks) של 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 ב-JVM המקומי, כוללTestPlayerRunHelper,RobolectricUtil,ShadowMediaCodecConfig,CapturingRenderersFactoryו-PlaybackOutput.
חשוב לוודא שהמודול כולל גם את יחסי התלות של מסגרת הבדיקה בסביבות הבדיקה:
- Robolectric:
org.robolectric:robolectric. צריך להגדיר אתandroid.testOptions.unitTests.isIncludeAndroidResources = trueכשמטעינים נכסים מקומיים. - thought
AndroidX Test:
androidx.test:runner,androidx.test:coreו-androidx.test.ext:junit. - שגרות המשך (coroutines) ב-Kotlin בשרשור הראשי:
org.jetbrains.kotlinx:kotlinx-coroutines-android.
הצטרפות לממשקי API לא יציבים לבדיקה
מחלקות ו-methods ב-media3-test-utils וב-media3-test-utils-robolectric מסומנות ב-@UnstableApi. למרות שהמטרה של Media3 היא לשמור על הארגונומיה של כלי העזר הנפוצים לבדיקה בכל הגרסאות, כלי העזר לבדיקה לא פועלים לפי אותם כללי יציבות של תאימות בינארית כמו ממשקי ה-API להפעלה בסביבת הייצור.
בקבוצות המקורות לבדיקה, מוסיפים הערות לשיטות או למחלקות הבדיקה באמצעות @androidx.annotation.OptIn, מוסיפים הסכמה של package-info לחבילות הבדיקה או מגדירים את lint.xml למקורות הבדיקה. מידע נוסף מופיע בהערות לפתרון בעיות ב-lint של API לא יציב.
בדיקות מקומיות באמצעות Robolectric
בדיקות Robolectric מקומיות פועלות ב-JVM של מחשב הפיתוח, בלי מכשיר פיזי או אמולטור. 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, צריך להגדיר קודקים מדומים ותיעוד פלט באופן הבא:
- מוסיפים את
withAllDefaultSupportedCodecsמ-ShadowMediaCodecConfigבתור@Ruleשל JUnit כדי לרשום את הקודקים המדומים של Robolectric. - מעבירים את
CapturingRenderersFactoryשנוצר באמצעותClock(CapturingRenderersFactory(context, builder.getClock())) של ה-builder אלbuilder.setRenderersFactory. - כשבודקים טראקים של וידאו, צריך לצרף
Surface(כמוSurface(SurfaceTexture(1))) עםplayer.setVideoSurfaceכדי ש-MediaCodecVideoRendererיעבד פריימים מפוענחים במקום לדלג עליהם. - כדי לאמת את הפלט שעבר עיבוד, צריך להפעיל את
register(player, capturingRenderersFactory)ב-PlaybackOutputלפני שמתחילים את ההפעלה. - בסיום ההפעלה, משחררים את הנגן ואת ה-Surface, ואז בודקים את הפלט המעוצב מ-
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 ושגרות המשך (coroutine)
בבדיקות אינסטרומנטציה, ה-Looper הראשי פועל בזמן אמת במכשיר.
אין לכלול את Robolectric או את media3-test-utils-robolectric ביחסי התלות של androidTestImplementation. כדי להמתין למצבי הפעלה ולאירועים, media3-test-utils מספק את פונקציות העזר הבאות:
- Kotlin: השעיית פונקציות של תוספים ב-
Player(כמוawaitPlaybackState,awaitIsPlaying,awaitFirstFrameRenderedו-awaitContentPositionAtLeast). - Java: ה-method הסטטי
futureWhen(player)ב-PlayerFence, שמחזירPlayerFenceשצריך לקרוא ל-methods של התנאים שלו (כמוentersPlaybackStateו-isPlaying) ב-thread של הנגןLooper, ומחזירListenableFutureשה-thread של הבדיקה יכול לחסום באמצעות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כדי לא לפספס אירוע מוקדם של פריים חד-פעמי. - תנאים בהתאמה אישית: עוטפים listener בהתאמה אישית או המתנות של שגרת המשך (coroutine) בתוך
awaitSupervisedב-playerב-Kotlin, או מעביריםListenableFutureבהתאמה אישית אלfutureWhen(player).withSupervision(future)ב-Java. כשבודקים את השחזור משגיאות לא חמורות, מעבירים אתfailOnNonFatalErrors = false(Kotlin) או קוראים ל-ignoringNonFatalErrors(Java), ומתאימים אישית את הזמן הקצוב לתפוגה באמצעותtimeoutאוwithTimeoutMs(timeoutMs).
בדוגמה הבאה, המערכת ממתינה לאירוע זמני של פריים ראשון ומפקחת על פונקציית event listener של מטא-נתונים בהתאמה אישית:
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
כשבודקים רכיבים של ממשק המשתמש כמו רכיבים קומפוזביליים של PlayerView או media3-ui-compose, ViewModels או קריאות חוזרות (callbacks) של MediaSession, בדרך כלל אין צורך ליצור ExoPlayer מלא. FakePlayer ב-media3-test-utils מרחיב את SimpleBasePlayer כדי לדמות פלייליסטים, מצבי הפעלה, מיקומים, גדלים של סרטונים, אותות ושגיאות ב-Looper של ה-thread ששולח את הקריאה. ה-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 כולל פונקציות עזר מיוחדות לתרחישי בדיקה אחרים:
- Feature flags ומצב Canary: משתמשים ב-
Media3FlagsRuleיחד עם@BindFlagו-@BindCanaryModeכדי לאפס את שינויי ברירת המחדל של ה-flag אחרי כל בדיקה, ולהריץ בדיקות עם פרמטרים במצבים שונים של ה-flag. מידע נוסף זמין במאמר בדיקה באמצעות דגלי תכונות. - בדיקות חוזים בהתאמה אישית של
DataSource: מחלקת משנה שלDataSourceContractTestכדי לוודא שההטמעות בהתאמה אישית שלDataSourceאוHttpDataSourceתואמות לציפיות של החוזה של Media3 בבקשות טווח, בטיפול בשגיאות ובמאזינים להעברה. מידע נוסף זמין במאמר שילוב מקבצי רשת אחרים.