الاختبار

توفّر 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 وإخراج الصوت وإدارة الحقوق الرقمية على الأجهزة الحقيقية اختبارات قياس حالة التطبيق على الأجهزة الفعلية (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 عند تحميل مواد العرض المحلية.
  • AndroidX Test: androidx.test:runner وandroidx.test:core وandroidx.test.ext:junit
  • أنماط "كوروتين" في Kotlin على سلسلة التعليمات الرئيسية: org.jetbrains.kotlinx:kotlinx-coroutines-android.

الموافقة على استخدام واجهات برمجة التطبيقات الاختبارية غير المستقرة

تتم إضافة التعليقات التوضيحية إلى الفئات والطرق في media3-test-utils وmedia3-test-utils-robolectric باستخدام @UnstableApi. على الرغم من أنّ Media3 تهدف إلى الحفاظ على سهولة استخدام أدوات المساعدة في الاختبارات المستخدَمة على نطاق واسع في جميع الإصدارات، فإنّ أدوات الاختبار لا تتّبع قواعد ثبات التوافق الثنائي نفسها التي تتّبعها واجهات برمجة التطبيقات الخاصة بتشغيل الوسائط في مرحلة الإنتاج.

في مجموعات رموز المصدر الاختبارية، يمكنك إضافة تعليقات توضيحية إلى فئات الاختبار أو طرقه باستخدام @androidx.annotation.OptIn، أو إضافة موافقة package-info لحِزم الاختبار، أو ضبط lint.xml لمصادر الاختبار. لمزيد من المعلومات، يُرجى الاطّلاع على ملاحظات تحديد المشاكل وحلّها في واجهة برمجة التطبيقات غير المستقرة.

الاختبارات المحلية باستخدام Robolectric

يتم إجراء اختبارات Robolectric المحلية على جهاز Java الافتراضي (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). توفّر أنواع النتائج هذه طرقًا للانتظار في حالات التشغيل والمواضع وعمليات نقل قائمة التشغيل والإطارات المعروضة والأوامر في انتظار المراجعة وأخطاء التشغيل المتوقعة أو تجاهل الأخطاء غير الفادحة. لمزيد من المعلومات حول شروط الانتظار، يُرجى الاطّلاع على مرجع واجهة برمجة التطبيقات 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 كـ @Rule في JUnit لتسجيل برامج الترميز التي يحاكيها 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 المحلية برامج فك الترميز الحقيقية للمنصة أو المقاطع الصوتية أو جلسات إدارة الحقوق الرقمية أو عرض 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.

تشرف كلتا واجهتَي برمجة التطبيقات على اللاعب أثناء الانتظار. في حال حدوث خطأ فادح في التشغيل (أو خطأ غير فادح تلقائيًا) أو إذا تجاوز الانتظار المهلة المحدّدة (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

عند اختبار مكوّنات واجهة المستخدم، مثل الدوال المركّبة PlayerView أو media3-ui-compose، أو ViewModels، أو عمليات الاستدعاء 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 في ما يتعلّق بطلبات النطاق ومعالجة الأخطاء ومستمعي عمليات النقل. لمزيد من المعلومات، يُرجى الاطّلاع على دمج حزم الشبكات الأخرى.