الزاوي: اختبار الأشياء غير المتزامنة في المنطقة المزيفة VS. توفير جدولة مخصصة

لقد طرحت عدة مرات أسئلة حول "المنطقة المزيفة" وكيفية استخدامها. لهذا السبب قررت أن أكتب هذا المقال لمشاركة ملاحظاتي عندما يتعلق الأمر باختبارات "وهمية مزيفة" ذات الحبيبات الدقيقة.

المنطقة جزء أساسي من النظام البيئي الزاوي. ربما قرأ المرء أن المنطقة نفسها ليست سوى "سياق التنفيذ". في الواقع ، monkeypatches الوظائف العامة مثل setTimeout أو setInterval من أجل اعتراض الوظائف التي يتم تنفيذها بعد بعض التأخير (setTimeout) أو بشكل دوري (setInterval).

من المهم أن نذكر أن هذا المقال لن يوضح كيفية التعامل مع المتسللين setTimeout. نظرًا لأن Angular تستخدم بشكل مكثف RxJs الذي يعتمد على وظائف التوقيت الأصلية (قد تفاجأ ولكن هذا صحيح) ، فإنه يستخدم المنطقة كأداة معقدة لكنها قوية لتسجيل جميع الإجراءات غير المتزامنة التي قد تؤثر على حالة التطبيق. الزاوي يعترضهم من أجل معرفة ما إذا كان لا يزال هناك بعض العمل في قائمة الانتظار. يستنزف قائمة الانتظار حسب الوقت. على الأرجح ، المهام المستنزفة تغيير قيم متغيرات المكون. نتيجة لذلك ، يتم إعادة تقديم القالب.

الآن ، كل الأشياء غير المتزامنة ليست هي ما نحتاج إلى القلق بشأنه. من الجيد فهم ما يحدث تحت الغطاء لأنه يساعد في كتابة اختبارات فعالة للوحدة. علاوة على ذلك ، يكون للتطوير الذي يحركه الاختبار تأثير كبير على الكود المصدر („كانت أصول TDD رغبة في الحصول على اختبار انحدار تلقائي قوي يدعم التصميم التطوري. "مارتن فاولر ، https://martinfowler.com/articles/mocksArentStubs.html ، 09/2017).

نتيجة لكل هذه الجهود ، يمكننا تغيير الوقت حيث نحتاج إلى اختبار الحالة في وقت محدد.

مخطط fakeAsync / التجزئة

تنص مستندات Angular على أن fakeAsync (https://angular.io/guide/testing#fake-async) يوفر تجربة ترميز أكثر خطية لأنه يتخلص من الوعود مثل .whenStable (). ثم (...).

يبدو الرمز داخل كتلة fakeAsync كما يلي:

ضع علامة (100)؛ // انتظر المهمة الأولى لإنجازها
fixture.detectChanges ()؛ // عرض التحديث مع اقتباس
ضع علامة ()؛ // انتظر المهمة الثانية لإنجازها
fixture.detectChanges ()؛ // عرض التحديث مع اقتباس

القصاصات التالية تعطي بعض الأفكار عن الطريقة التي يعمل بها fakeAsync.

setTimeout / setInterval يتم استخدامها هنا لأنها تظهر بوضوح عندما يتم تنفيذ الوظائف في منطقة fakeAsync. قد تتوقع أن تعرف هذه الوظيفة "it" متى يتم الاختبار (في Jasmine مرتبة حسب الوسيطة التي تم إجراؤها: Function) ولكن هذه المرة نعتمد على رفيق fakeAsync بدلاً من استخدام أي نوع من رد الاتصال:

هو ('يستنزف مهمة المنطقة حسب المهمة' ، fakeAsync (() => {
        setTimeout (() => {
            دعني = 0 ؛
            const const = setInterval (() => {
                إذا (i ++ === 5) {
                    clearInterval (مقبض)؛
                }
            } ، 1000) ؛
        } ، 10000) ؛
}))؛

يشكو بصوت عال لأنه لا يزال هناك بعض "المؤقتات" (= setTimeouts) في قائمة الانتظار:

خطأ: لا يزال هناك مؤقت واحد في قائمة الانتظار.

من الواضح أننا نحتاج إلى تغيير الوقت لإنجاز وظيفة المهلة. نقوم بإلحاق "علامة" المعلمة بـ 10 ثوانٍ:

ضع علامة (10000)؛

هيو؟ الخطأ يصبح أكثر مربكة. الآن ، يفشل الاختبار بسبب "الموقتات الدورية" المحددة (= setIntervals)

خطأ: لا يزال هناك مؤقت مؤقت واحد في قائمة الانتظار.

نظرًا لأننا حددنا وظيفة ما يجب تنفيذه كل ثانية ، فإننا نحتاج أيضًا إلى تحويل الوقت باستخدام العلامة مرة أخرى. وظيفة تنتهي نفسها بعد 5 ثوان. لهذا السبب نحتاج إلى إضافة 5 ثوانٍ أخرى:

ضع علامة (15000)؛

الآن ، الاختبار يمر. تجدر الإشارة إلى أن المنطقة تتعرف على المهام الجارية بالتوازي. مجرد تمديد مهلة وظيفة من خلال مكالمة setInterval أخرى.

هو ('يستنزف مهمة المنطقة حسب المهمة' ، fakeAsync (() => {
    setTimeout (() => {
        دعني = 0 ؛
        const const = setInterval (() => {
            إذا (++ i === 5) {
                clearInterval (مقبض)؛
            }
        } ، 1000) ؛
        دع j = 0 ؛
        const handle2 = setInterval (() => {
            إذا (++ j === 3) {
                clearInterval (handle2)؛
            }
        } ، 1000) ؛
    } ، 10000) ؛
    ضع علامة (15000)؛
}))؛

الاختبار لا يزال يمر لأن كلا من setIntervals قد بدأ في نفس الوقت. يتم كلاهما عند مرور 15 ثانية:

fakeAsync / وضع علامة في العمل

الآن نحن نعرف كيف تعمل fakeAsync / tick stuff. ندعه يستخدم لبعض الأشياء ذات مغزى.

دعنا نطور حقلًا يشبه الاقتراح يستوفي هذه المتطلبات:

  • ينتزع النتيجة من بعض API (الخدمة)
  • يؤدي إلى اختناق إدخال المستخدم من أجل انتظار مصطلح البحث النهائي (يقلل من عدد الطلبات) ؛ DEBOUNCING_VALUE = 300
  • يعرض النتيجة في واجهة المستخدم وتنبعث منها الرسالة المناسبة
  • يحترم اختبار الوحدة الطبيعة غير المتزامنة للشفرة ويختبر السلوك المناسب لحقل الإقتراح من حيث الوقت المنقضي

انتهى بنا المطاف مع سيناريوهات الاختبار هذه:

صف ("عند البحث" ، () => {
    it ('امسح النتيجة السابقة' ، fakeAsync (() => {
    }))؛
    it ('تنبعث منها إشارة البداية' ، fakeAsync (() => {
    }))؛
    it ('هو اختناق الزيارات المحتملة لواجهة برمجة التطبيقات لطلب واحد لكل DEBOUNCING_VALUE ميلي ثانية' ، fakeAsync (() => {
    }))؛
})؛
صف ("على النجاح" ، () => {
    it ('يستدعي google API' ، fakeAsync (() => {
    }))؛
    (تنبعث منها إشارة النجاح بعدد المطابقات) ، fakeAsync (() => {
    }))؛
    it ('يُظهر العناوين في حقل الإقتراح' ، fakeAsync (() => {
    }))؛
})؛
صف ("على خطأ" ، () => {
    ((تنبعث منها إشارة الخطأ) ، fakeAsync (() => {
    }))؛
})؛

في "البحث" ، لا ننتظر نتيجة البحث. عندما يوفر المستخدم إدخالًا (على سبيل المثال ، "Lon") ، يلزم مسح الخيارات السابقة. نتوقع أن تكون الخيارات فارغة. بالإضافة إلى ذلك ، يجب أن يتم اختناق إدخال المستخدم ، دعنا نقول بقيمة 300 ميلي ثانية. فيما يتعلق بالمنطقة ، يتم دفع 300 ميكروتسكوت في قائمة الانتظار.

لاحظ أنني أغفلت بعض التفاصيل عن الإيجاز:

  • يعد إعداد الاختبار هو نفسه كما يظهر في مستندات Angular
  • يتم حقن مثيل apiService من خلال fixture.debugElement.injector (...)
  • SpecUtils يطلق الأحداث المتعلقة بالمستخدم مثل الإدخال والتركيز
قبل كل (() => {
    spyOn (apiService ، 'query'). and.returnValue (Observable.of (queryResult)) ؛
})؛
مناسب ('مسح النتيجة السابقة' ، fakeAsync (() => {
    comp.options = ['غير فارغ'] ؛
    SpecUtils.focusAndInput ('Lon' ، تركيبات ، 'إدخال') ؛
    ضع علامة (DEBOUNCING_VALUE)؛
    fixture.detectChanges ()؛
    توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛
}))؛

رمز المكون الذي يحاول تلبية الاختبار:

ngOnInit () {
    this.control.valueChanges.debounceTime (300). الاشتراك (value => {
        this.options = [] ؛
        this.suggest (القيمة)؛
    })؛
}
أقترح (ف: سلسلة) {
    this.googleBooksAPI.query (q) .subscribe (result => {
// ...
    } ، () => {
// ...
    })؛
}

دعنا نذهب من خلال التعليمات البرمجية خطوة بخطوة:

نحن نتجسس على طريقة الاستعلام apiService التي سنستدعيها في المكون. يحتوي الاستعلام queryResult على بعض البيانات الوهمية مثل "Hamlet" و "Macbeth" و "King Lear". في البداية ، نتوقع أن تكون الخيارات فارغة ، ولكن ربما لاحظت أن قائمة انتظار fakeAsync بأكملها تستنزف بعلامة (DEBOUNCING_VALUE) وبالتالي يحتوي المكون على النتيجة النهائية لكتابات شكسبير أيضًا:

من المتوقع أن تكون 3 صفر ، "كان [هاملت ، ماكبث ، الملك لير]".

نحتاج إلى تأخير لطلب استعلام الخدمة من أجل محاكاة مرور غير متزامن للوقت الذي تستغرقه مكالمة API. دعنا نضيف تأخيرًا لمدة 5 ثوانٍ (REQUEST_DELAY = 5000) وضع علامة (5000).

قبل كل (() => {
    spyOn (apiService، 'query'). and.returnValue (Observable.of (queryResult) .delay (1000))؛
})؛

مناسب ('مسح النتيجة السابقة' ، fakeAsync (() => {
    comp.options = ['غير فارغ'] ؛
    SpecUtils.focusAndInput ('Lon' ، تركيبات ، 'إدخال') ؛
    ضع علامة (DEBOUNCING_VALUE)؛
    fixture.detectChanges ()؛
    توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛
    ضع علامة (REQUEST_DELAY)؛
}))؛

في رأيي ، يجب أن يعمل هذا المثال ولكن تدعي Zone.js أنه لا يزال هناك بعض العمل في قائمة الانتظار:

خطأ: لا يزال هناك مؤقت مؤقت واحد في قائمة الانتظار.

في هذه المرحلة ، نحتاج إلى التعمق لرؤية تلك الوظائف التي نشك في تعلقها بالمنطقة. وضع بعض نقاط التوقف هو السبيل للذهاب:

تصحيح fakeAsync المنطقة

ثم ، قم بإصدار هذا في سطر الأوامر

_fakeAsyncTestZoneSpec._scheduler._schedulerQueue [0] .args [0] [0]

أو فحص محتوى المنطقة مثل هذا:

هممم ، طريقة تدفق AsyncScheduler ما زالت في قائمة الانتظار ... لماذا؟

اسم الوظيفة التي يتم وضعها هي طريقة تدفق AsyncScheduler.

التدفق العام (الإجراء: AsyncAction ): void {
  const {Actions} = هذا ؛
  إذا كان (this.active) {
    actions.push (العمل)؛
    إرجاع؛
  }
  اسمحوا خطأ: أي ؛
  this.active = صحيح ؛
  فعل {
    if (error = action.execute (action.state، action.delay)) {
      استراحة؛
    }
  } في حين (action = Actions.shift ()) ؛ // استنفد قائمة انتظار المجدول
  this.active = false ؛
  إذا (خطأ) {
    بينما (الإجراء = Actions.shift ()) {
      action.unsubscribe ()؛
    }
    خطأ رمي
  }
}

الآن ، قد تتساءل عن الخطأ في الكود المصدر أو المنطقة نفسها.

المشكلة هي أن المنطقة وعلامات التجزئة لدينا غير متزامنة.

تحتوي المنطقة نفسها على الوقت الحالي (2017) ، بينما تريد القراد معالجة الإجراء المقرر في 01.01.1970 + 300 ملي + 5 ثوانٍ.

تؤكد قيمة برنامج جدولة المتزامن ما يلي:

استيراد {async كـ AsyncScheduler} من 'rxjs / scheduler / async' ؛
// ضع هذا في مكان ما داخل "إنه"
console.info (AsyncScheduler.now ())؛
// → 1503235213879

AsyncZoneTimeInSyncKeeper إلى الإنقاذ

أحد الحلول الممكنة لهذا هو الحصول على أداة مساعدة للمزامنة مثل:

فئة التصدير AsyncZoneTimeInSyncKeeper {
    الوقت = 0 ؛
    البناء() {
        spyOn (AsyncScheduler ، 'الآن'). and.callFake (() => {
            / * tslint: تعطيل السطر التالي * /
            console.info ('الوقت' ، this.time) ؛
            العودة this.time.
        })؛
    }
    وضع علامة (الوقت ؟: رقم) {
        if (typeof time! == 'undefined') {
            هذا.وقت + = الوقت ؛
            ضع علامة (this.time)؛
        } آخر {
            ضع علامة ()؛
        }
    }
}

يقوم بتتبع الوقت الحالي الذي يتم إرجاعه بواسطة now () كلما تم استدعاء برنامج جدولة المتزامن. هذا يعمل لأن الدالة tick () تستخدم نفس الوقت الحالي. كلاهما ، المجدول والمنطقة ، تشترك في نفس الوقت.

أوصي بتثبيته في timeInSyncKeeper في المرحلة السابقة:

صف ("عند البحث" ، () => {
    دع timeInSyncKeeper ؛
    قبل كل (() => {
        timeInSyncKeeper = جديد AsyncZoneTimeInSyncKeeper () ؛
    })؛
})؛

الآن ، دعونا نلقي نظرة على استخدام حارس مزامنة الوقت. ضع في اعتبارك أنه يتعين علينا معالجة مشكلة التوقيت هذه لأن حقل النص قد تم رفضه وأن الطلب يستغرق بعض الوقت.

صف ("عند البحث" ، () => {
    دع timeInSyncKeeper ؛
    قبل كل (() => {
        timeInSyncKeeper = جديد AsyncZoneTimeInSyncKeeper () ؛
        spyOn (apiService ، 'query'). and.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY))؛
    })؛
    it ('امسح النتيجة السابقة' ، fakeAsync (() => {
        comp.options = ['غير فارغ'] ؛
        SpecUtils.focusAndInput ('Lon' ، تركيبات ، 'إدخال') ؛
        timeInSyncKeeper.tick (DEBOUNCING_VALUE)؛
        fixture.detectChanges ()؛
        توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛
        timeInSyncKeeper.tick (REQUEST_DELAY)؛
    }))؛
    // ...
})؛

دعنا نذهب من خلال هذا المثال سطرا بسطر:

  1. إنشاء مثيل لحافظة المزامنة
timeInSyncKeeper = جديد AsyncZoneTimeInSyncKeeper () ؛

2. دعنا نرد على طريقة apiService.query مع الاستعلام عن النتائج بعد انتهاء REQUEST_DELAY. دعنا نقول أن طريقة الاستعلام بطيئة وتستجيب بعد REQUEST_DELAY = 5000 مللي ثانية.

spyOn (apiService ، 'query'). and.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY))؛

3. التظاهر بوجود خيار "غير فارغ" في حقل "الاقتراح"

comp.options = ['غير فارغ'] ؛

4. انتقل إلى حقل "الإدخال" في العنصر الأصلي للتركيبات وأدخل القيمة "خط الطول". هذا يحاكي تفاعل المستخدم مع حقل الإدخال.

SpecUtils.focusAndInput ('Lon' ، تركيبات ، 'إدخال') ؛

5. اترك الفترة الزمنية DEBOUNCING_VALUE في منطقة المزامنة المزيفة (DEBOUNCING_VALUE = 300 ميلي ثانية).

timeInSyncKeeper.tick (DEBOUNCING_VALUE)؛

6. كشف التغييرات وإعادة تقديم قالب HTML.

fixture.detectChanges ()؛

7. مجموعة الخيارات فارغة الآن!

توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛

وهذا يعني أن القيمة الملحوظةالتغييرات المستخدمة في المكونات قد تمت إدارتها في الوقت المناسب. لاحظ أن وظيفة debounceTime-d المنفذة

القيمة => {
    this.options = [] ؛
    this.onEvent.emit ({إشارة: SuggestSignal.start}) ؛
    this.suggest (القيمة)؛
}

دفعت مهمة أخرى إلى قائمة الانتظار عن طريق استدعاء الأسلوب توحي:

أقترح (ف: سلسلة) {
    إذا (! ف) {
        إرجاع؛
    }
    this.googleBooksAPI.query (q) .subscribe (result => {
        إذا (النتيجة) {
            this.options = result.items.map (item => item.volumeInfo) ؛
            this.onEvent.emit ({إشارة: SuggestSignal.success، totalItems: result.totalItems})؛
        } آخر {
            this.onEvent.emit ({إشارة: SuggestSignal.success، totalItems: 0})؛
        }
    } ، () => {
        this.onEvent.emit ({إشارة: SuggestSignal.error}) ؛
    })؛
}

فقط تذكر التجسس على طريقة استعلام واجهة برمجة تطبيقات كتب google التي تستجيب بعد 5 ثوانٍ.

8. أخيرًا ، يتعين علينا تحديد مرة أخرى من أجل REQUEST_DELAY = 5000 مللي ثانية من أجل مسح قائمة انتظار المنطقة. تحتاج الملاحظة التي نشترك بها في طريقة الاقتراح إلى REQUEST_DELAY = 5000 لإكمالها.

timeInSyncKeeper.tick (REQUEST_DELAY)؛

fakeAsync ...؟ لماذا ا؟ هناك جدولة!

قد يجادل خبراء ReactiveX بأنه بإمكاننا استخدام برامج جدولة الاختبار لجعل الملاحظات قابلة للاختبار. إنه ممكن للتطبيقات الزاوية ، لكن له بعض العيوب:

  • يتطلب منك التعرف على البنية الداخلية للملاحظات والمشغلين ...
  • ماذا لو كان لديك بعض الحلول القبيحة setTimeout في التطبيق الخاص بك؟ لم يتم التعامل معها من قبل المجدولين.
  • الأكثر أهمية: أنا متأكد من أنك لا تريد استخدام أدوات الجدولة في التطبيق بالكامل. لا تريد مزج كود الإنتاج مع اختبارات وحدتك. أنت لا تريد أن تفعل شيئا مثل هذا:
اختبار const
if (environment.test) {
    testScheduler = جديد YourTestScheduler () ؛
}
دعنا نلاحظ
if (testScheduler) {
    يمكن ملاحظتها = يمكن ملاحظتها. ("القيمة"). التأخير (1000 ، testScheduler)
} آخر {
    ملاحظه = ملاحظه. ("القيمه". التأخير (1000) ؛
}

هذا ليس حلا قابلا للتطبيق. في رأيي ، فإن الحل الوحيد الممكن هو "ضخ" جدولة الاختبار من خلال توفير نوع من "الوكلاء" لطرق Rxjs الحقيقية. شيء آخر يجب أخذه في الاعتبار هو أن الطرق الغالبة قد تؤثر سلبًا على اختبارات الوحدة المتبقية. لهذا السبب سوف نستخدم جواسيس ياسمين. الحصول على تطهير الجواسيس بعد كل ذلك.

التفاف الدالة monkeypatchScheduler تطبيق Rxjs الأصلي باستخدام جاسوس. يأخذ الجاسوس وسيطات الأسلوب ويضيف testScheduler إذا كان ذلك مناسبًا.

استيراد {IScheduler} من 'rxjs / Scheduler' ؛
استيراد {Observable} من 'rxjs / Observable' ؛
أعلن فار spyOn: وظيفة ؛
وظيفة التصدير monkeypatchScheduler (جدولة: IScheduler) {
    دع obsableMethods = ['concat' ، 'defer' ، 'blank' ، 'forkJoin' ، 'if' ، 'interval' ، 'merge' ، 'of' ، 'range' ، 'رمي' ،
        "الرمز البريدي"].
    اسمح operatorMethods = ['buffer' ، 'concat' ، 'تأخير' ، 'متميز' ، 'فعل' ، 'كل' ، 'أخير' ، 'دمج' ، 'ماكس' ، 'خذ' ،
        "timeInterval" ، "lift" ، "debounceTime"] ؛
    واسمحوا injectFn = وظيفة (الأساس: أي ، الطرق: سلسلة []) {
        methods.forEach (method => {
            const orig = base [method]؛
            إذا (typeof orig === 'function') {
                spyOn (الأساس ، الطريقة). و .allFake (function () {
                    واسمحوا args = Array.prototype.slice.call (الوسائط) ؛
                    if (args [args.length - 1] && typeof args [args.length - 1] .now === 'function') {
                        args [args.length - 1] = المجدول ؛
                    } آخر {
                        args.push (جدولة)؛
                    }
                    return orig.apply (this، args)؛
                })؛
            }
        })؛
    }؛
    injectFn (يمكن ملاحظتها ، طرق يمكن ملاحظتها) ؛
    injectFn (Observable.prototype، operatorMethods)؛
}

من الآن فصاعدًا ، سيقوم testScheduler بتنفيذ جميع الأعمال داخل Rxjs. لا يستخدم setTimeout / setInterval أو أي نوع من الأشياء غير المتزامنة. ليس هناك ضرورة ل fakeAsync بعد الآن.

الآن ، نحن بحاجة إلى مثيل لجدولة الاختبار الذي نريد تمريره إلى monkeypatchScheduler.

يتصرف إلى حد كبير مثل TestScheduler الافتراضي ولكنه يوفر طريقة رد الاتصال onAction. بهذه الطريقة ، نعرف الإجراء الذي تم تنفيذه بعد هذه الفترة الزمنية.

فئة التصدير SpyingTestScheduler يمتد VirtualTimeScheduler {
    spyFn: (actionName: string، delay: number، error ؟: any) => void؛
    البناء() {
        super (VirtualAction، defaultMaxFrame)؛
    }
    onAction (spyFn: (actionName: string، delay: number، error ؟: any) => void) {
        this.spyFn = spyFn؛
    }
    فورة - غزير - وفير() {
        const {Actions، maxFrames} = هذا ؛
        اسمحوا خطأ: أي ، الإجراء: AsyncAction ؛
        بينما ((action = Actions.shift ()) && (this.frame = action.delay) <= maxFrames) {
            واسمحوا stateName = this.detectStateName (الإجراء)؛
            اسمحوا تأخير = action.delay ؛
            if (error = action.execute (action.state، action.delay)) {
                if (this.spyFn) {
                    this.spyFn (stateName ، تأخير ، خطأ) ؛
                }
                استراحة؛
            } آخر {
                if (this.spyFn) {
                    this.spyFn (stateName ، تأخير) ؛
                }
            }
        }
        إذا (خطأ) {
            بينما (الإجراء = Actions.shift ()) {
                action.unsubscribe ()؛
            }
            خطأ رمي
        }
    }
    detectStateName الخاص (الإجراء: AsyncAction ): string {
        const c = Object.getPrototypeOf (action.state) .constructor؛
        const argsPos = c.toString (). indexOf ('(')؛
        إذا (argsPos! == -1) {
            return c.toString (). substring (9، argsPos)؛
        }
        عودة لاغية
    }
}

أخيرًا ، دعونا نلقي نظرة على الاستخدام. المثال هو نفس اختبار الوحدة كما كان مستخدمًا من قبل (("مسح النتيجة السابقة") مع وجود اختلاف بسيط في أننا سنستخدم برنامج جدولة الاختبار بدلاً من fakeAsync / tick.

اسمحوا testScheduler.
قبل كل (() => {
    testScheduler = جديد SpyingTestScheduler () ؛
    testScheduler.maxFrames = 1000000؛
    monkeypatchScheduler (testScheduler)؛
    fixture.detectChanges ()؛
})؛
قبل كل (() => {
    spyOn (apiService ، 'query'). and.callFake (() => {
        return Observable.of (queryResult) .delay (REQUEST_DELAY)؛
    })؛
})؛
ذلك ('مسح النتيجة السابقة' ، (تم التنفيذ: الوظيفة) => {
    comp.options = ['غير فارغ'] ؛
    testScheduler.onAction ((actionName: string، delay: number، err ؟: any) => {
        if (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
            توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛
            فعله()؛
        }
    })؛
    SpecUtils.focusAndInput ('Londo' ، تركيبات ، 'إدخال') ؛
    fixture.detectChanges ()؛
    testScheduler.flush ()؛
})؛

يتم إنشاء جدولة الاختبار و monkeypatched (!) في الأول قبل كل. في الثانية السابقة ، كلنا نتجسس على apiService.query لكي نعرض استعلام النتيجةالنتائج بعد REQUEST_DELAY = 5000 ميلي ثانية.

الآن ، دعنا نذهب عبر السطر بسطر:

  1. بادئ ذي بدء ، لاحظ أننا نعلن عن القيام بالوظيفة التي نحتاجها بالاقتران مع رد الفعل المبرمج لجدولة الاختبار. هذا يعني أننا بحاجة إلى إخبار ياسمين بأن الاختبار يتم من تلقاء أنفسنا.
ذلك ('مسح النتيجة السابقة' ، (تم التنفيذ: الوظيفة) => {

2. مرة أخرى ، نتظاهر ببعض الخيارات الموجودة في المكون.

comp.options = ['غير فارغ'] ؛

3. هذا يتطلب بعض التفسير لأنه يبدو خرقاء قليلاً من النظرة الأولى. نريد أن ننتظر إجراءً يسمى "DebounceTimeSubscriber" مع تأخير DEBOUNCING_VALUE = 300 مللي ثانية. عندما يحدث هذا ، نريد أن نتحقق مما إذا كانت options.length تساوي 0. بعد ذلك ، يتم الانتهاء من الاختبار ونطلق عليه علامة ().

testScheduler.onAction ((actionName: string، delay: number، err ؟: any) => {
    if (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
      توقع (comp.options.length) .toBe (0 ، `كان [$ {comp.options.join ('،')}]`)؛
      فعله()؛
    }
})؛

سترى أن استخدام برامج جدولة الاختبار يتطلب بعض المعرفة الخاصة حول تطبيق Rxjs الداخلي. يعتمد هذا بالطبع على ماهية برنامج اختبار الجدولة الذي تستخدمه ، ولكن حتى إذا قمت بتنفيذ برنامج جدولة قوي بنفسك ، فسوف تحتاج إلى فهم المجدولين وفضح بعض قيم وقت التشغيل للمرونة (والتي ، مرة أخرى ، قد لا تكون بديهية).

4. مرة أخرى ، يقوم المستخدم بإدخال القيمة "Londo".

SpecUtils.focusAndInput ('Londo' ، تركيبات ، 'إدخال') ؛

5. مرة أخرى ، اكتشف التغييرات وأعد تقديم القالب.

fixture.detectChanges ()؛

6. أخيرًا ، ننفذ جميع الإجراءات الموضوعة في قائمة انتظار المجدول.

testScheduler.flush ()؛

ملخص

إن أدوات الاختبار الخاصة بـ Angular هي الأفضل من الأدوات الذاتية الصنع ... طالما أنها تعمل. في بعض الحالات ، لا يعمل الزوجان المزيفان / المزيفان لكن لا يوجد سبب يدعو إلى اليأس وإلغاء اختبارات الوحدة. في هذه الحالات ، تكون أداة المساعدة في المزامنة التلقائية (والمعروفة أيضًا باسم AsyncZoneTimeInSyncKeeper) أو برنامج جدولة اختبار مخصص (تعرف هنا أيضًا باسم SpyingTestScheduler) هي الطريقة التي يجب اتباعها.

مصدر الرمز