Angular: ທົດສອບສິ່ງຂອງ async ໃນເຂດ fakedAsync VS. ສະ ໜອງ ເຄື່ອງ ກຳ ນົດເວລາທີ່ ກຳ ນົດເອງ

ຂ້ອຍໄດ້ຖືກຖາມຫຼາຍຄັ້ງກ່ຽວກັບເຂດ "ເຂດປອມ" ແລະວິທີການໃຊ້ມັນ. ນັ້ນແມ່ນເຫດຜົນທີ່ຂ້ອຍຕັດສິນໃຈຂຽນບົດຄວາມນີ້ເພື່ອແບ່ງປັນການສັງເກດການຂອງຂ້ອຍເມື່ອກ່ຽວກັບການທົດສອບທີ່ຖືກປັບ ໄໝ„ fakeAsync.

ເຂດດັ່ງກ່າວແມ່ນສ່ວນ ໜຶ່ງ ທີ່ ສຳ ຄັນຂອງລະບົບນິເວດ Angular. ທ່ານອາດຈະໄດ້ອ່ານວ່າເຂດຕົວຂອງມັນເອງແມ່ນພຽງແຕ່ປະເພດຂອງ "ສະພາບການປະຕິບັດ". ໃນຄວາມເປັນຈິງ, Angular monkeypatches ຫນ້າທີ່ທົ່ວໂລກເຊັ່ນ setTimeout ຫຼື setInterval ເພື່ອສະກັດກັ້ນການເຮັດວຽກທີ່ຖືກປະຕິບັດພາຍຫຼັງທີ່ມີຄວາມລ່າຊ້າ (setTimeout) ຫຼືແຕ່ລະໄລຍະ (setInterval).

ມັນເປັນສິ່ງ ສຳ ຄັນທີ່ຈະກ່າວເຖິງວ່າບົດຂຽນນີ້ບໍ່ໄດ້ສະແດງວິທີການຈັດການກັບ hacks ທີ່ ກຳ ນົດ. ເນື່ອງຈາກ Angular ໃຊ້ຢ່າງຮຸນແຮງຂອງ RxJs ທີ່ອີງໃສ່ ໜ້າ ທີ່ການ ກຳ ນົດເວລາທີ່ ກຳ ເນີດ (ທ່ານອາດຈະແປກໃຈແຕ່ມັນເປັນຄວາມຈິງ), ມັນໃຊ້ເຂດທີ່ເປັນເຄື່ອງມືທີ່ສັບສົນແຕ່ມີປະສິດທິພາບສູງໃນການບັນທຶກການປະຕິບັດທີ່ບໍ່ຄືກັນເຊິ່ງອາດຈະສົ່ງຜົນກະທົບຕໍ່ສະພາບການ ນຳ ໃຊ້. Angular ຂັດຂວາງພວກເຂົາເພື່ອໃຫ້ຮູ້ວ່າມັນຍັງມີວຽກຢູ່ໃນແຖວຢູ່ບໍ? ມັນຈະຖີ້ມແຖວນັ້ນຂື້ນກັບເວລາ. ສ່ວນຫຼາຍອາດຈະ, ວຽກທີ່ລະບາຍນ້ ຳ ປ່ຽນຄຸນຄ່າຂອງຕົວປ່ຽນສ່ວນປະກອບ. ດ້ວຍເຫດນັ້ນ, ແມ່ແບບໄດ້ຮັບການຕອບແທນຄືນ.

ໃນປັດຈຸບັນ, ທຸກໆສິ່ງຂອງ async ບໍ່ແມ່ນສິ່ງທີ່ພວກເຮົາຕ້ອງກັງວົນ. ມັນເປັນການດີທີ່ຈະເຂົ້າໃຈວ່າມີຫຍັງເກີດຂື້ນພາຍໃຕ້ຜ້າອ້ອມເພາະມັນຊ່ວຍໃນການຂຽນ ໜ່ວຍ ທົດສອບທີ່ມີປະສິດຕິພາບ. ຍິ່ງໄປກວ່ານັ້ນ, ການພັດທະນາແບບທົດລອງມີຜົນກະທົບຢ່າງຫຼວງຫຼາຍຕໍ່ລະຫັດແຫຼ່ງຂໍ້ມູນ (D ຕົ້ນ ກຳ ເນີດຂອງ TDD ແມ່ນຄວາມປາຖະ ໜາ ທີ່ຈະໄດ້ຮັບການທົດສອບການສືບພັນແບບອັດຕະໂນມັດທີ່ແຂງແຮງເຊິ່ງສະ ໜັບ ສະ ໜູນ ການອອກແບບວິວັດທະນາການ. “ Martin Fowler, https://martinfowler.com/articles/mocksArentStubs.html, 09/2017).

ເປັນຜົນມາຈາກຄວາມພະຍາຍາມທັງ ໝົດ ນີ້ພວກເຮົາສາມາດປ່ຽນເວລາໄດ້ຍ້ອນວ່າພວກເຮົາຕ້ອງການທົດສອບສະຖານະການໃນຈຸດເວລາສະເພາະ.

ໂຄງການ fakeAsync / ໝາຍ ຕິກ

ເອກະສານ Angular ລະບຸວ່າ fakeAsync (https://angular.io/guide/testing#fake-async) ມີປະສົບການໃນການຂຽນໂຄ້ດທີ່ມີເສັ້ນຊື່ຫຼາຍຂື້ນເພາະມັນຖືກ ກຳ ຈັດ ຄຳ ສັນຍາຕ່າງໆເຊັ່ນ .whenStable (). ແລ້ວ (…).

ລະຫັດພາຍໃນ blockAsync block ມີລັກສະນະດັ່ງນີ້:

ໝາຍ ຕິກ (100); // ລໍຖ້າວຽກ ທຳ ອິດ ສຳ ເລັດ
fixture.detectChanges (); // update ເບິ່ງດ້ວຍ ຄຳ ອ້າງອີງ
tick (); // ລໍຖ້າວຽກທີສອງ ສຳ ເລັດ
fixture.detectChanges (); // update ເບິ່ງດ້ວຍ ຄຳ ອ້າງອີງ

ຂໍ້ສະ ເໜີ ຫຍໍ້ຕໍ່ໄປນີ້ໃຫ້ຄວາມຮູ້ບາງຢ່າງກ່ຽວກັບວິທີການເຮັດວຽກຂອງ fakeAsync.

setTimeout / setInterval ກຳ ລັງຖືກ ນຳ ໃຊ້ຢູ່ນີ້ເພາະວ່າມັນສະແດງໃຫ້ເຫັນຢ່າງຈະແຈ້ງວ່າເມື່ອ ໜ້າ ທີ່ຖືກປະຕິບັດຢູ່ໃນເຂດ fakeAsync. ທ່ານອາດຄາດຫວັງວ່າ ໜ້າ ທີ່ "ມັນ" ນີ້ຕ້ອງຮູ້ເວລາການທົດສອບເຮັດ (ໃນ Jasmine ຈັດລຽງໂດຍການໂຕ້ຖຽງທີ່ເຮັດແລ້ວ: ໜ້າ ທີ່) ແຕ່ເວລານີ້ພວກເຮົາເພິ່ງພາກັບເພື່ອນຮ່ວມງານຂອງ fakeAsync ແທນທີ່ຈະໃຊ້ການເອີ້ນຄືນແບບໃດ:

ມັນ ('ຂັບໄລ່ເຂດຕາມ ໜ້າ ວຽກ', fakeAsync (() => {
        setTimeout (() => {
            ໃຫ້ i = 0;
            const handle = setInterval (() => {
                ຖ້າ (i ++ === 5) {
                    clearInterval (ຈັດການ);
                }
            }, 1000);
        }, 10000);
}));

ມັນຈົ່ມສຽງດັງເພາະວ່າມັນຍັງມີບາງເຄື່ອງຈັບເວລາ (= setTimeouts) ຢູ່ໃນແຖວ:

ຂໍ້ຜິດພາດ: 1 timer (s) ຍັງຢູ່ໃນແຖວ.

ມັນເຫັນໄດ້ຊັດວ່າພວກເຮົາຕ້ອງປ່ຽນເວລາເພື່ອເຮັດ ໜ້າ ທີ່ ໝົດ ເວລາໃຫ້ ສຳ ເລັດ. ພວກເຮົາເພີ່ມເຕີມ "tick" ຕົວກໍານົດການທີ່ມີ 10 ວິນາທີ:

ໝາຍ ຕິກ (10000);

ຮື? ຂໍ້ຜິດພາດດັ່ງກ່າວຍິ່ງສັບສົນຂື້ນ. ໃນປັດຈຸບັນ, ການທົດສອບລົ້ມເຫລວຍ້ອນວ່າ "ເຄື່ອງຈັບເວລາແຕ່ລະໄລຍະ" (= setIntervals) ທີ່ຖືກລວບລວມ:

ຂໍ້ຜິດພາດ: 1 timer timer ຍັງຢູ່ໃນແຖວ.

ເນື່ອງຈາກວ່າພວກເຮົາໄດ້ປະຕິບັດ ໜ້າ ທີ່ສິ່ງທີ່ຕ້ອງໄດ້ປະຕິບັດໃນທຸກໆວິນາທີພວກເຮົາກໍ່ຕ້ອງປ່ຽນເວລາໂດຍການໃຊ້ ໝາຍ ຕິກອີກຄັ້ງ. ຫນ້າທີ່ສິ້ນສຸດລົງຕົວຂອງມັນເອງຫຼັງຈາກ 5 ວິນາທີ. ນັ້ນແມ່ນເຫດຜົນທີ່ພວກເຮົາຕ້ອງເພີ່ມອີກ 5 ວິນາທີ:

ໝາຍ ຕິກ (15000);

ດຽວນີ້, ການທົດສອບ ກຳ ລັງຜ່ານໄປ. ມັນເປັນມູນຄ່າທີ່ຈະເວົ້າວ່າເຂດດັ່ງກ່າວຮັບຮູ້ວຽກງານທີ່ແລ່ນພ້ອມກັນ. ພຽງແຕ່ຂະຫຍາຍເວລາເຮັດວຽກ ໝົດ ເວລາໂດຍການຕັ້ງຄ່າໂທ callInterval ອື່ນ.

ມັນ ('ຂັບໄລ່ເຂດຕາມ ໜ້າ ວຽກ', fakeAsync (() => {
    setTimeout (() => {
        ໃຫ້ i = 0;
        const handle = setInterval (() => {
            ຖ້າ (++ i === 5) {
                clearInterval (ຈັດການ);
            }
        }, 1000);
        ໃຫ້ j = 0;
        const handle2 = setInterval (() => {
            ຖ້າ (++ j === 3) {
                clearInterval (handle2);
            }
        }, 1000);
    }, 10000);
    ໝາຍ ຕິກ (15000);
}));

ການທົດສອບແມ່ນຍັງຜ່ານໄປເພາະວ່າທັງສອງ setIntervals ເຫຼົ່ານັ້ນໄດ້ເລີ່ມຕົ້ນໃນເວລາດຽວກັນ. ທັງສອງຂອງພວກເຂົາແມ່ນເຮັດໄດ້ເມື່ອ 15 ວິນາທີຜ່ານໄປ:

fakeAsync / ຫມາຍຕິກໃນການປະຕິບັດ

ໃນປັດຈຸບັນພວກເຮົາຮູ້ວິທີການປອມແປງເຄື່ອງຫມາຍ / ຫມາຍຕິກເຮັດວຽກໄດ້ດີ. ໃຫ້ມັນໃຊ້ ສຳ ລັບບາງສິ່ງທີ່ມີຄວາມ ໝາຍ.

ຂໍໃຫ້ພັດທະນາຂະ ແໜງ ການທີ່ແນະ ນຳ ທີ່ຕອບສະ ໜອງ ຄວາມຕ້ອງການເຫຼົ່ານີ້:

  • ມັນຈັບຜົນໄດ້ຮັບຈາກບາງ API (ບໍລິການ)
  • ມັນປິດການປ້ອນຂໍ້ມູນຂອງຜູ້ໃຊ້ເພື່ອລໍຖ້າ ຄຳ ຄົ້ນຫາສຸດທ້າຍ (ມັນຫຼຸດ ຈຳ ນວນ ຄຳ ຂໍ); DEBOUNCING_VALUE = 300
  • ມັນສະແດງໃຫ້ເຫັນຜົນໃນ UI ແລະສົ່ງຂໍ້ຄວາມທີ່ ເໝາະ ສົມ
  • ການທົດສອບຫົວ ໜ່ວຍ ແມ່ນເຄົາລົບລັກສະນະຂອງລະຫັດແລະທົດສອບພຶດຕິ ກຳ ທີ່ ເໝາະ ສົມຂອງພາກສະ ໜາມ ທີ່ແນະ ນຳ ໃນແງ່ຂອງເວລາທີ່ຜ່ານໄປ

ພວກເຮົາສິ້ນສຸດດ້ວຍສະຖານະການທົດສອບນີ້:

ອະທິບາຍ ('ໃນການຄົ້ນຫາ', () => {
    ມັນ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', fakeAsync (() => {
    }));
    ມັນ ('ປ່ອຍສັນຍານເລີ່ມຕົ້ນ', fakeAsync (() => {
    }));
    ມັນ ('ກຳ ລັງຈະຖືກລື່ນຄາດ ໝາຍ ຂອງ API ເຖິງ 1 ຄຳ ຂໍຕໍ່ຄັ້ງຕໍ່ DEBOUNCING_VALUE milliseconds', fakeAsync (() => {
    }));
});
ອະທິບາຍ ('ກ່ຽວກັບຄວາມ ສຳ ເລັດ', () => {
    ມັນ ('ເອີ້ນ google API', fakeAsync (() => {
    }));
    ມັນ ('ປ່ອຍສັນຍານຄວາມ ສຳ ເລັດກັບ ຈຳ ນວນການແຂ່ງຂັນ', fakeAsync (() => {
    }));
    ມັນ ('ສະແດງໃຫ້ເຫັນຫົວຂໍ້ໃນພາກສະຫນາມແນະນໍາ', fakeAsync (() => {
    }));
});
ອະທິບາຍ ('ກ່ຽວກັບຄວາມຜິດ', () => {
    ມັນ ('ປ່ອຍສັນຍານຄວາມຜິດພາດ', fakeAsync (() => {
    }));
});

ໃນ "ການຄົ້ນຫາ" ພວກເຮົາບໍ່ລໍຖ້າຜົນການຄົ້ນຫາ. ເມື່ອຜູ້ໃຊ້ສະ ໜອງ ວັດສະດຸປ້ອນ (ຕົວຢ່າງ on Lon)) ທາງເລືອກທີ່ຜ່ານມາຕ້ອງມີການເກັບກູ້. ພວກເຮົາຄາດຫວັງວ່າຕົວເລືອກຈະຫວ່າງຢູ່. ນອກຈາກນັ້ນ, ການປ້ອນຂໍ້ມູນຂອງຜູ້ໃຊ້ຕ້ອງໄດ້ຖືກຖິ້ມ, ໃຫ້ເວົ້າດ້ວຍມູນຄ່າ 300 ມິນລິລິດ. ໃນແງ່ຂອງເຂດດັ່ງກ່າວ, ຂະ ໜາດ 300 ມິນລີແມັດກ້ອນຖືກຍູ້ເຂົ້າແຖວ.

ໝາຍ ເຫດ, ຂ້ອຍຍົກເວັ້ນບາງລາຍລະອຽດ ສຳ ລັບຄວາມແຕກຕ່າງ:

  • ການຕິດຕັ້ງການທົດສອບແມ່ນຂ້ອນຂ້າງຄືກັນກັບທີ່ເຫັນໃນ Angular docs
  • ຕົວຢ່າງ apiService ແມ່ນຖືກສັກຜ່ານ fixture.debugElement.injector (…)
  • SpecUtils ກະຕຸ້ນເຫດການທີ່ກ່ຽວຂ້ອງກັບຜູ້ໃຊ້ເຊັ່ນ: ການປ້ອນຂໍ້ມູນແລະຈຸດສຸມ
beforeEach (() => {
    spyOn (apiService, 'query') ແລະ and.returnValue (Observable.of (queryResult));
});
ພໍດີ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', fakeAsync (() => {
    comp.options = ['ບໍ່ຫວ່າງ'];
    SpecUtils.focusAndInput ('Lon', fixture, 'input');
    ໝາຍ ຕິກ (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);
}));

ລະຫັດສ່ວນປະກອບທີ່ພະຍາຍາມເພື່ອທົດສອບຄວາມພໍໃຈ:

ngOnInit () {
    this.control.valueChanges.debounceTime (300) .subscribe (ມູນຄ່າ => {
        this.options = [];
        this.suggest (ມູນຄ່າ);
    });
}
ແນະ ນຳ (q: ຊ່ອຍແນ່) {
    this.googleBooksAPI.query (q) .subscribe (ຜົນໄດ້ຮັບ => {
// ...
    }, () => {
// ...
    });
}

ຂໍໃຫ້ລະຫັດຜ່ານແຕ່ລະບາດກ້າວ:

ພວກເຮົາ spy ກ່ຽວກັບວິທີການສອບຖາມ apiService ເຊິ່ງພວກເຮົາຈະໂທຫາໃນສ່ວນປະກອບ. ຕົວປ່ຽນແບບສອບຖາມຕົວປ່ຽນແປງປະກອບດ້ວຍຂໍ້ມູນທີ່ຖືກເຍາະເຍີ້ຍເຊັ່ນວ່າ "Hamlet", "Macbeth" ແລະ "King Lear". ໃນຕອນເລີ່ມຕົ້ນພວກເຮົາຄາດຫວັງວ່າຕົວເລືອກຕ່າງໆຈະບໍ່ມີປະໂຫຍດແຕ່ວ່າທ່ານອາດຈະໄດ້ສັງເກດວ່າຄິວເກົ່າປອມທັງ ໝົດ ໄດ້ຖືກຖີ້ມດ້ວຍ ໝາຍ ຕິກ (DEBOUNCING_VALUE) ແລະດັ່ງນັ້ນສ່ວນປະກອບມີຜົນສຸດທ້າຍຂອງການຂຽນຂອງ Shakespeare ເຊັ່ນດຽວກັນ:

ຄາດວ່າ 3 ຈະເປັນ 0, 'ແມ່ນ [Hamlet, Macbeth, King Lear]'.

ພວກເຮົາຕ້ອງການຄວາມລ່າຊ້າ ສຳ ລັບ ຄຳ ຮ້ອງຂໍການບໍລິການເພື່ອທີ່ຈະເຮັດຕາມການໃຊ້ເວລາທີ່ບໍ່ໄດ້ໃຊ້ເວລາໂດຍການໂທ API ໃຫ້ເພີ່ມຄວາມຊັກຊ້າ 5 ວິນາທີ (REQUEST_DELAY = 5000) ແລະ ໝາຍ ຕິກ (5000).

beforeEach (() => {
    spyOn (apiService, 'query') ແລະ and.returnValue (Observable.of (queryResult) .delay (1000));
});

ພໍດີ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', fakeAsync (() => {
    comp.options = ['ບໍ່ຫວ່າງ'];
    SpecUtils.focusAndInput ('Lon', fixture, 'input');
    ໝາຍ ຕິກ (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);
    ໝາຍ ຕິກ (REQUEST_DELAY);
}));

ໃນຄວາມຄິດເຫັນຂອງຂ້ອຍ, ຕົວຢ່າງນີ້ຄວນຈະເຮັດວຽກແຕ່ Zone.js ອ້າງວ່າຍັງມີບາງວຽກຢູ່ໃນແຖວ:

ຂໍ້ຜິດພາດ: 1 timer timer ຍັງຢູ່ໃນແຖວ.

ໃນຈຸດນີ້ພວກເຮົາຕ້ອງເຂົ້າໄປເບິ່ງເລິກກວ່າເພື່ອເບິ່ງ ໜ້າ ທີ່ເຫຼົ່ານັ້ນທີ່ພວກເຮົາສົງໃສວ່າຈະຕິດຢູ່ໃນເຂດ. ການ ກຳ ນົດຈຸດພັກຜ່ອນບາງຈຸດແມ່ນວິທີທີ່ຈະໄປ:

debugging ເຂດ fakeAsync

ຫຼັງຈາກນັ້ນ, ອອກນີ້ຢູ່ໃນບັນທັດຄໍາສັ່ງ

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

ຫຼືກວດກາເນື້ອໃນຂອງເຂດດັ່ງກ່າວ:

Hmmm, ວິທີການໄຫຼຂອງ AsyncScheduler ຍັງຢູ່ໃນແຖວ…ເປັນຫຍັງ?

ຊື່ຂອງ ໜ້າ ທີ່ທີ່ຖືກລວບລວມແມ່ນວິທີການໄຫລຂອງ AsyncScheduler.

ກະແສສາທາລະນະ (ການກະ ທຳ: AsyncAction ): void {
  const {ການກະ ທຳ} = ນີ້;
  ຖ້າ (this.active) {
    action.push (ການກະ ທຳ);
    ກັບມາ;
  }
  ໃຫ້ຄວາມຜິດພາດ: ມີ;
  this.active = true;
  ເຮັດ {
    ຖ້າ (ຂໍ້ຜິດພາດ = action.execute (action.state, action.delay)) {
      ພັກຜ່ອນ;
    }
  } ໃນຂະນະທີ່ (action = action.shift ()); // ໝົດ ກຳ ນົດເວລາແຖວແຖວ
  this.active = ບໍ່ຖືກຕ້ອງ;
  ຖ້າ (ຂໍ້ຜິດພາດ) {
    ໃນຂະນະທີ່ (action = action.shift ()) {
      action.unsubscribe ();
    }
    ຖິ້ມຄວາມຜິດພາດ;
  }
}

ດຽວນີ້, ທ່ານອາດຈະສົງໄສວ່າມັນຜິດຫຍັງກັບລະຫັດແຫຼ່ງຫລືເຂດນັ້ນເອງ.

ບັນຫາແມ່ນວ່າເຂດແລະຫມາຍຕິກຂອງພວກເຮົາແມ່ນບໍ່ກົງກັນ.

ເຂດຕົວມັນເອງມີເວລາປະຈຸບັນ (2017) ໃນຂະນະທີ່ຫມາຍຕິກຕ້ອງການທີ່ຈະປະຕິບັດການປະຕິບັດທີ່ກໍານົດໄວ້ໃນ 01.01.1970 + 300 ມິນລິລິດ + 5 ວິນາທີ.

ມູນຄ່າຂອງຜູ້ຈັດຕາຕະລາງ async ຢືນຢັນວ່າ:

ນຳ ເຂົ້າ {async ເປັນ AsyncScheduler} ຈາກ 'rxjs / scheduler / async';
// ຈັດວາງບ່ອນນີ້ພາຍໃນ "ມັນ"
console.info (AsyncScheduler.now ());
// → 1503235213879

AsyncZoneTimeInSyncKeeper ເພື່ອກູ້ໄພ

ການແກ້ໄຂທີ່ເປັນໄປໄດ້ ສຳ ລັບສິ່ງນີ້ແມ່ນການມີຜົນປະໂຫຍດໃນການເກັບຂໍ້ມູນແບບນີ້:

ຊັ້ນຮຽນສົ່ງອອກ AsyncZoneTimeInSyncKeeper {
    time = 0;
    ຜູ້ກໍ່ສ້າງ () {
        spyOn (AsyncScheduler, 'ດຽວນີ້'). and.callFake (() => {
            / * tslint: disable-next-line * /
            console.info ('ເວລາ', ເວລານີ້);
            ກັບຄືນ this.time;
        });
    }
    ໝາຍ ຕິກ (ເວລາ?: ເລກ ໝາຍ) {
        ຖ້າ (ເວລາ!! == 'ບໍ່ໄດ້ ກຳ ນົດ') {
            this.time + = ເວລາ;
            ໝາຍ ຕິກ (ເວລານີ້);
        } ອີກ {
            tick ();
        }
    }
}

ມັນຕິດຕາມເວລາໃນປະຈຸບັນທີ່ໄດ້ຮັບການສົ່ງຄືນໂດຍປັດຈຸບັນ () ທຸກຄັ້ງທີ່ຜູ້ວາງແຜນ async ຖືກເອີ້ນ. ນີ້ເຮັດວຽກໄດ້ເພາະວ່າ ໜ້າ ທີ່ ໝາຍ ຕິກ () ໃຊ້ເວລາດຽວກັນ. ທັງສອງ, ຜູ້ ກຳ ນົດເວລາແລະເຂດ, ແບ່ງປັນເວລາດຽວກັນ.

ຂ້ອຍຂໍແນະ ນຳ ໃຫ້ໃຊ້ເວລາໃນໄວທີ່ສຸດໃນໄລຍະກ່ອນກ່ອນເວລາ:

ອະທິບາຍ ('ໃນການຄົ້ນຫາ', () => {
    ໃຫ້ເວລາ INSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = ໃໝ່ AsyncZoneTimeInSyncKeeper ();
    });
});

ໃນປັດຈຸບັນ, ໃຫ້ພວກເຮົາເບິ່ງການ ນຳ ໃຊ້ຂອງຜູ້ຮັກສາການຊິ້ງຂໍ້ມູນເວລາ. ຈົ່ງຈື່ໄວ້ວ່າພວກເຮົາຕ້ອງໄດ້ແກ້ໄຂບັນຫາການ ກຳ ນົດເວລານີ້ເພາະວ່າເນື້ອໃນຂໍ້ຄວາມແມ່ນຖືກຖົກຖຽງແລະ ຄຳ ຮ້ອງຂໍຕ້ອງໃຊ້ເວລາບາງເວລາ.

ອະທິບາຍ ('ໃນການຄົ້ນຫາ', () => {
    ໃຫ້ເວລາ INSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = ໃໝ່ AsyncZoneTimeInSyncKeeper ();
        spyOn (apiService, 'query') ແລະ and.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY));
    });
    ມັນ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', fakeAsync (() => {
        comp.options = ['ບໍ່ຫວ່າງ'];
        SpecUtils.focusAndInput ('Lon', fixture, 'input');
        timeInSyncKeeper.tick (DEBOUNCING_VALUE);
        fixture.detectChanges ();
        expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);
        timeInSyncKeeper.tick (REQUEST_DELAY);
    }));
    // ...
});

ຂໍໃຫ້ຜ່ານຕົວຢ່າງເສັ້ນນີ້ຕາມ ລຳ ດັບ:

  1. ກະຕຸ້ນຕົວຢ່າງຂອງຕົວເກັບຂໍ້ມູນແບບ sync
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. ໄປທີ່ "ວັດສະດຸປ້ອນ" ໃນສ່ວນປະກອບພື້ນເມືອງຂອງສິ່ງແວດລ້ອມແລະໃສ່ຄ່າ "Lon". ນີ້ ຈຳ ລອງການໂຕ້ຕອບຂອງຜູ້ໃຊ້ກັບພາກສະ ໜາມ ປ້ອນຂໍ້ມູນ.

SpecUtils.focusAndInput ('Lon', fixture, 'input');

5. ໃຫ້ຜ່ານໄລຍະເວລາຂອງ DEBOUNCING_VALUE ໃນເຂດ async ປອມ (DEBOUNCING_VALUE = 300 milliseconds).

timeInSyncKeeper.tick (DEBOUNCING_VALUE);

6. ກວດພົບການປ່ຽນແປງແລະປັບ ໃໝ່ HTML template.

fixture.detectChanges ();

7. ແຖວຕົວເລືອກແມ່ນຫວ່າງດຽວນີ້!

expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);

ນີ້ຫມາຍຄວາມວ່າມູນຄ່າການສັງເກດການທີ່ໃຊ້ໃນສ່ວນປະກອບຕ່າງໆໄດ້ຖືກຈັດການໃນເວລາທີ່ ເໝາະ ສົມ. ໃຫ້ສັງເກດວ່າການປະຕິບັດງານ debounceTime-d

ມູນຄ່າ => {
    this.options = [];
    this.onEvent.emit ({ສັນຍານ: SuggestSignal.start});
    this.suggest (ມູນຄ່າ);
}

ຍູ້ວຽກອື່ນເຂົ້າໃນແຖວໂດຍການໂທຫາວິທີການແນະ ນຳ:

ແນະ ນຳ (q: ຊ່ອຍແນ່) {
    ຖ້າ (! q) {
        ກັບມາ;
    }
    this.googleBooksAPI.query (q) .subscribe (ຜົນໄດ້ຮັບ => {
        ຖ້າ (ຜົນໄດ້ຮັບ) {
            this.options = result.items.map (item => item.volumeInfo);
            this.onEvent.emit ({ສັນຍານ: SuggestSignal.success, totalItems: result.totalItems});
        } ອີກ {
            this.onEvent.emit ({ສັນຍານ: SuggestSignal.success, ລວມທັງ ໝົດ: 0});
        }
    }, () => {
        this.onEvent.emit ({ສັນຍານ: SuggestSignal.error});
    });
}

ພຽງແຕ່ຈື່ລະຫັດ spy ຂອງ google google API ແບບສອບຖາມວິທີການຕອບສະ ໜອງ ພາຍຫຼັງ 5 ວິນາທີ.

8. ໃນທີ່ສຸດ, ພວກເຮົາຕ້ອງໄດ້ ໝາຍ ຕິກອີກເທື່ອ ໜຶ່ງ ສຳ ລັບ REQUEST_DELAY = 5000 ມິນລິລິດເພື່ອທີ່ຈະລຽນແຖວແຖວ. ການສັງເກດການທີ່ພວກເຮົາລົງທະບຽນໃນວິທີການແນະ ນຳ ຕ້ອງການ REQUEST_DELAY = 5000 ເພື່ອໃຫ້ ສຳ ເລັດ.

timeInSyncKeeper.tick (REQUEST_DELAY);

fakeAsync …? ຍ້ອນຫຍັງ? ມີຜູ້ ກຳ ນົດເວລາ!

ຜູ້ຊ່ຽວຊານດ້ານ ReactiveX ອາດຈະໂຕ້ຖຽງວ່າພວກເຮົາສາມາດ ນຳ ໃຊ້ຕາຕະລາງການທົດສອບເພື່ອເຮັດໃຫ້ຜູ້ສັງເກດການສາມາດທົດສອບໄດ້. ມັນເປັນໄປໄດ້ ສຳ ລັບແອບພິເຄຊັນ Angular ແຕ່ມັນມີຂໍ້ເສຍປຽບບາງຢ່າງ:

  • ມັນຮຽກຮ້ອງໃຫ້ທ່ານເພື່ອໃຫ້ໄດ້ຮັບຄຸ້ນເຄີຍກັບໂຄງສ້າງພາຍໃນຂອງຜູ້ສັງເກດການ, ຜູ້ປະຕິບັດງານ, …
  • ຈະເປັນແນວໃດຖ້າວ່າທ່ານມີ workTimeout setTimeout ທີ່ບໍ່ດີໃນແອັບພລິເຄຊັນຂອງທ່ານ? ພວກມັນບໍ່ໄດ້ຖືກຈັດການໂດຍຜູ້ ກຳ ນົດເວລາ.
  • ສິ່ງທີ່ ສຳ ຄັນທີ່ສຸດ: ຂ້ອຍແນ່ໃຈວ່າທ່ານບໍ່ຕ້ອງການໃຊ້ເຄື່ອງ ກຳ ນົດເວລາໃນໂປແກຼມຂອງທ່ານທັງ ໝົດ. ທ່ານບໍ່ຕ້ອງການປົນລະຫັດການຜະລິດກັບການທົດສອບຫົວ ໜ່ວຍ ຂອງທ່ານ. ທ່ານບໍ່ຕ້ອງການເຮັດສິ່ງນີ້:
const testScheduler;
ຖ້າ (environment.test) {
    testScheduler = New YourTestScheduler ();
}
ປ່ອຍໃຫ້ສັງເກດ;
ຖ້າ (testScheduler) {
    Obsableable = Observable.of ('ມູນຄ່າ') ຄວາມລ່າຊ້າ (1000, testScheduler)
} ອີກ {
    observable = Observable.of ('ມູນຄ່າ') ຄວາມລ່າຊ້າ (1000);
}

ນີ້ບໍ່ແມ່ນທາງອອກທີ່ ເໝາະ ສົມ. ໃນຄວາມຄິດເຫັນຂອງຂ້ອຍ, ການແກ້ໄຂບັນຫາທີ່ເປັນໄປໄດ້ພຽງແຕ່ແມ່ນ "ສັກ" ຕົວ ກຳ ນົດເວລາການທົດສອບໂດຍການສະ ໜອງ "ຕົວແທນ" ສຳ ລັບວິທີການທີ່ແທ້ຈິງຂອງ Rxjs. ສິ່ງ ໜຶ່ງ ອີກທີ່ຕ້ອງ ຄຳ ນຶງເຖິງແມ່ນວ່າວິທີການທີ່ເກີນ ກຳ ນົດອາດສົ່ງຜົນກະທົບທາງລົບຕໍ່ການທົດສອບຫົວ ໜ່ວຍ ທີ່ຍັງເຫຼືອ. ນັ້ນແມ່ນເຫດຜົນທີ່ພວກເຮົາຈະໃຊ້ຄົນສອດແນມ Jasmine. Spies ໄດ້ຮັບການລ້າງຫຼັງຈາກມັນທຸກ.

ຟັງຊັນ monkeypatchScheduler ຫໍ່ການປະຕິບັດ Rxjs ເດີມໂດຍໃຊ້ spy. ຄົນສອດແນມໃຊ້ເວລາການໂຕ້ຖຽງຂອງວິທີການແລະອຸທອນ testScheduler ຖ້າ ເໝາະ ສົມ.

ນຳ ເຂົ້າ {IScheduler} ຈາກ 'rxjs / Scheduler';
ນຳ ເຂົ້າ {Observable} ຈາກ 'rxjs / Observable';
ປະກາດ var spyOn: Function;
ຟັງຊັນການສົ່ງອອກ monkeypatchScheduler (ຕາຕະລາງ: IScheduler) {
    let observableMet methods = ['concat', 'defer', 'empty', 'forkJoin', 'if', 'interval', 'merge', 'of', 'range', 'throw',
        'zip'];
    ໃຫ້ operatorMet methods = ['buffer', 'concat', 'delay', 'ແຕກຕ່າງ', 'ເຮັດ', 'ທຸກໆ', 'ສຸດທ້າຍ', 'ຮວມກັນ', 'ສູງສຸດ', 'ເອົາ',
        'timeInterval', 'ຍົກ', 'debounceTime'];
    ໃຫ້ injectFn = ໜ້າ ທີ່ (ຖານ: ໃດ, ວິທີການ: string []) {
        methods.forEach (ວິທີການ => {
            const orig = ຖານ [ວິທີການ];
            ຖ້າ (typeof orig === 'ຟັງຊັນ') {
                spyOn (ຖານ, ວິທີການ) .and.callFake (function () {
                    let args = Array.prototype.slice.call (ການໂຕ້ຖຽງ);
                    ຖ້າ (ໂຕ້ຖຽງ [ໂຕ້ຖຽງກັນ - 1] && typeof ໂຕ້ຖຽງ [args.length - 1] .now === 'ຟັງຊັນ') {
                        args [args.length - 1] = ຜູ້ ກຳ ນົດເວລາ;
                    } ອີກ {
                        args.push (ຜູ້ ກຳ ນົດເວລາ);
                    }
                    return orig.apply (ນີ້, ໂຕ້ຖຽງ);
                });
            }
        });
    };
    injectFn (ສັງເກດ, ວິທີການທີ່ສາມາດສັງເກດໄດ້);
    injectFn (Observable.prototype, operatorMet methods);
}

ຈາກນີ້, testScheduler ຈະປະຕິບັດວຽກງານທັງ ໝົດ ພາຍໃນ Rxjs. ມັນບໍ່ໄດ້ໃຊ້ setTimeout / setInterval ຫຼືສິ່ງຂອງ async ໃດໆ. ບໍ່ມີຄວາມ ຈຳ ເປັນ ສຳ ລັບການປອມແປງອີກຕໍ່ໄປ.

ໃນປັດຈຸບັນ, ພວກເຮົາຕ້ອງການຕົວຢ່າງຕາຕະລາງການທົດສອບທີ່ພວກເຮົາຕ້ອງການສົ່ງຜ່ານ monkeypatchScheduler.

ມັນມີພຶດຕິ ກຳ ຫຼາຍຄືກັບ TestScheduler ໃນຕອນຕົ້ນແຕ່ມັນສະ ໜອງ ວິທີການເອີ້ນຄືນ onAction. ວິທີນີ້, ພວກເຮົາຮູ້ວ່າການປະຕິບັດໃດທີ່ຖືກປະຕິບັດພາຍຫຼັງໄລຍະເວລາໃດຫນຶ່ງ.

ຫ້ອງຮຽນສົ່ງອອກ SpyingTestScheduler ຂະຫຍາຍ VirtualTimeScheduler {
    spyFn: (actionName: string, ຊັກຊ້າ: ຈຳ ນວນ, ຂໍ້ຜິດພາດ?: ໃດ) => ໂມຄະ;
    ຜູ້ກໍ່ສ້າງ () {
        super (VirtualAction, defaultMaxFrame);
    }
    onAction (spyFn: (actionName: string, ຊັກຊ້າ: ຈຳ ນວນ, ຂໍ້ຜິດພາດ?: ໃດ) => ໂມຄະ)
        this.spyFn = spyFn;
    }
    flush () {
        const {ການກະ ທຳ, maxFrames} = ນີ້;
        ໃຫ້ຂໍ້ຜິດພາດ: ມີ, ການກະ ທຳ: AsyncAction ;
        ໃນຂະນະທີ່ ((action = action.shift ()) && (this.frame = action.delay) <= maxFrames) {
            let stateName = this.detectStateName (ການກະ ທຳ);
            let delay = action.delay;
            ຖ້າ (ຂໍ້ຜິດພາດ = action.execute (action.state, action.delay)) {
                ຖ້າ (this.spyFn) {
                    this.spyFn (stateName, ຄວາມລ່າຊ້າ, ຄວາມຜິດພາດ);
                }
                ພັກຜ່ອນ;
            } ອີກ {
                ຖ້າ (this.spyFn) {
                    this.spyFn (stateName, ຄວາມລ່າຊ້າ);
                }
            }
        }
        ຖ້າ (ຂໍ້ຜິດພາດ) {
            ໃນຂະນະທີ່ (action = action.shift ()) {
                action.unsubscribe ();
            }
            ຖິ້ມຄວາມຜິດພາດ;
        }
    }
    private detectStateName (ການກະ ທຳ: AsyncAction ): string {
        const c = Object.getPrototypeOf (action.state). ຜູ້ກໍ່ສ້າງ;
        const argsPos = c.toString (). indexOf ('(');
        ຖ້າ (argsPos! == -1) {
            ກັບຄືນ c.toString (). (9, argsPos);
        }
        return null;
    }
}

ສຸດທ້າຍ, ໃຫ້ເບິ່ງການ ນຳ ໃຊ້. ຕົວຢ່າງແມ່ນການທົດສອບຫົວ ໜ່ວຍ ດຽວກັນກັບທີ່ໃຊ້ໃນເມື່ອກ່ອນ (ມັນ ('ລ້າງຜົນໄດ້ຮັບກ່ອນ ໜ້າ ນີ້') ດ້ວຍຄວາມແຕກຕ່າງເລັກນ້ອຍທີ່ພວກເຮົາ ກຳ ລັງຈະໃຊ້ຕາຕະລາງການທົດສອບແທນທີ່ຈະເປັນ fakeAsync / tick.

ໃຫ້ testScheduler;
beforeEach (() => {
    testScheduler = ໃໝ່ SpyingTestScheduler ();
    testScheduler.maxFrames = 1000000;
    monkeypatchScheduler (testScheduler);
    fixture.detectChanges ();
});
beforeEach (() => {
    spyOn (apiService, 'query'). ແລະ.callFake (() => {
        ກັບຄືນ Observable.of (queryResult) .delay (REQUEST_DELAY);
    });
});
ມັນ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', (ເຮັດແລ້ວ: ໜ້າ ທີ່) => {
    comp.options = ['ບໍ່ຫວ່າງ'];
    testScheduler.onAction ((actionName: string, ຊັກຊ້າ: ຈຳ ນວນ, ຜິດພາດ: ໃດ)) => {
        ຖ້າ (actionName === 'DebounceTimeSubscriber' && ຊັກຊ້າ === DEBOUNCING_VALUE) {
            expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);
            ເຮັດແລ້ວ ();
        }
    });
    SpecUtils.focusAndInput ('Londo', fixture, 'input');
    fixture.detectChanges ();
    testScheduler.flush ();
});

ຜູ້ ກຳ ນົດເວລາການທົດສອບຖືກສ້າງຂື້ນແລະ monkeypatched (!) ໃນຄັ້ງ ທຳ ອິດກ່ອນທີ່ຈະ ນຳ ໃຊ້. ໃນສອງວິນາທີກ່ອນ, ພວກເຮົາຕິດຕາມ apiService.query ເພື່ອຮັບໃຊ້ຜົນການຄົ້ນຫາຫຼັງຈາກ REQUEST_DELAY = 5000 milliseconds.

ໃນປັດຈຸບັນ, ໃຫ້ຂອງໄປໂດຍຜ່ານເສັ້ນມັນເສັ້ນ:

  1. ກ່ອນອື່ນ ໝົດ, ໃຫ້ສັງເກດວ່າພວກເຮົາປະກາດ ໜ້າ ທີ່ທີ່ເຮັດແລ້ວເຊິ່ງພວກເຮົາຕ້ອງການສົມທົບກັບການທົດສອບການເອີ້ນຄືນຂອງຕາຕະລາງການທົດສອບ. ນີ້ ໝາຍ ຄວາມວ່າພວກເຮົາ ຈຳ ເປັນຕ້ອງບອກ Jasmine ວ່າການທົດສອບແມ່ນເຮັດດ້ວຍຕົວເອງ.
ມັນ ('ລ້າງຜົນໄດ້ຮັບທີ່ຜ່ານມາ', (ເຮັດແລ້ວ: ໜ້າ ທີ່) => {

2. ອີກເທື່ອ ໜຶ່ງ, ພວກເຮົາ ທຳ ທ່າວ່າບາງຕົວເລືອກທີ່ມີຢູ່ໃນສ່ວນປະກອບ.

comp.options = ['ບໍ່ຫວ່າງ'];

3. ນີ້ຮຽກຮ້ອງໃຫ້ມີ ຄຳ ອະທິບາຍບາງຢ່າງເພາະວ່າມັນປະກົດວ່າບໍ່ຄ່ອຍຈະແຈ້ງໃນເວລາທີ່ເຫັນຕອນ ທຳ ອິດ. ພວກເຮົາຕ້ອງການລໍຖ້າການກະ ທຳ ທີ່ເອີ້ນວ່າ "DebounceTimeSubscriber" ດ້ວຍຄວາມຊັກຊ້າຂອງ DEBOUNCING_VALUE = 300 ມິນລິລິດ. ເມື່ອສິ່ງນີ້ເກີດຂື້ນ, ພວກເຮົາຕ້ອງການກວດສອບວ່າ options.length ແມ່ນ 0. ຫຼັງຈາກນັ້ນ, ການທົດສອບໄດ້ ສຳ ເລັດແລ້ວແລະພວກເຮົາກໍ່ເອີ້ນວ່າເຮັດແລ້ວ ().

testScheduler.onAction ((actionName: string, ຊັກຊ້າ: ຈຳ ນວນ, ຜິດພາດ: ໃດ)) => {
    ຖ້າ (actionName === 'DebounceTimeSubscriber' && ຊັກຊ້າ === DEBOUNCING_VALUE) {
      expect (comp.options.length) .toBe (0, `ແມ່ນ [$ {comp.options.join (',')}]`);
      ເຮັດແລ້ວ ();
    }
});

ທ່ານເຫັນວ່າການ ນຳ ໃຊ້ຕາຕະລາງການທົດສອບຮຽກຮ້ອງໃຫ້ມີຄວາມຮູ້ພິເສດບາງຢ່າງກ່ຽວກັບພາກສ່ວນການປະຕິບັດຂອງ Rxjs. ແນ່ນອນວ່າມັນຂື້ນກັບສິ່ງທີ່ໃຊ້ເວລາທົດສອບທີ່ທ່ານໃຊ້ແຕ່ເຖິງແມ່ນວ່າທ່ານຈະປະຕິບັດຕາຕະລາງທີ່ມີປະສິດທິພາບດ້ວຍຕົວທ່ານເອງທ່ານກໍ່ ຈຳ ເປັນຕ້ອງເຂົ້າໃຈຜູ້ ກຳ ນົດເວລາແລະເປີດເຜີຍຄ່ານິຍົມບາງເວລາ ສຳ ລັບຄວາມຍືດຫຍຸ່ນ (ເຊິ່ງອີກເທື່ອ ໜຶ່ງ ອາດຈະບໍ່ແມ່ນ ຄຳ ອະທິບາຍຂອງຕົວເອງ).

4. ອີກເທື່ອ ໜຶ່ງ, ຜູ້ໃຊ້ປ້ອນຄ່າ "Londo".

SpecUtils.focusAndInput ('Londo', fixture, 'input');

5. ອີກເທື່ອ ໜຶ່ງ, ກວດພົບການປ່ຽນແປງແລະຈັດຮູບແບບ ໃໝ່.

fixture.detectChanges ();

6. ສຸດທ້າຍ, ພວກເຮົາປະຕິບັດທຸກການກະ ທຳ ທີ່ວາງໄວ້ໃນແຖວຜູ້ ກຳ ນົດເວລາ.

testScheduler.flush ();

ບົດສະຫຼຸບ

ອຸປະກອນທົດສອບຂອງຕົວເອງແມ່ນເປັນທີ່ນິຍົມ ສຳ ລັບຜູ້ທີ່ຜະລິດເອງ ... ຕາບໃດທີ່ພວກເຂົາເຮັດວຽກ. ໃນບາງກໍລະນີຄູ່ຜົວເມຍ fakeAsync / tick ບໍ່ເຮັດວຽກແຕ່ບໍ່ມີເຫດຜົນທີ່ຈະ ໝົດ ຫວັງແລະຍົກເລີກການທົດສອບຫົວ ໜ່ວຍ. ໃນກໍລະນີເຫຼົ່ານີ້, ເຄື່ອງປະດັບທີ່ອັດຕະໂນມັດ (ທີ່ນີ້ເອີ້ນວ່າ AsyncZoneTimeInSyncKeeper) ຫຼືຜູ້ ກຳ ນົດການທົດສອບແບບ ກຳ ນົດເອງ (ທີ່ນີ້ຍັງຮູ້ວ່າ SpyingTestScheduler) ແມ່ນທາງທີ່ຈະໄປ.

ລະຫັດແຫຼ່ງຂໍ້ມູນ