كيفية اختبار الرمز الذي يحتوي على setTimeout / setInterval تحت غطاء المحرك

نحن المطورون مغرمون جدًا باختبارات الوحدات ، وفائدتها واضحة. ولكي تكون هذه الاختبارات مفيدة حقًا ، ولا تسبب ألمًا ، من الضروري ضمان استقرارها.


تقوم شركتنا بتطوير إطار عمل واجهة " الوسابي " وبيع المنتجات المبنية عليه ، وهي التطبيقات السحابية وتطبيقات سطح المكتب. ترتبط دورة التحرير بإحكام بالتقويم ، ويتم إعداد عمليات التكامل المستمر للتحكم في جودة المنتج. نستخدم Jenkins للتجميعات و Mocha بالتزامن مع تأكيد Chaiلاختبار وحدة رمز جافا سكريبت. ومؤخرًا ، واجهنا موقفًا بدأت فيه مراقبة التجميع في إظهار أن حوالي نصف جميع حالات سقوطها كانت بسبب اختبارات وحدة جافا سكريبت غير مستقرة. الأعراض هي نفسها: إما أن الاختبار المنفصل من المجموعة ليس لديه وقت للتشغيل ، أو يرجع النتيجة الخاطئة كما هو متوقع. ويكشف تحليل الحالات دائمًا تقريبًا عن حقيقة أن الاختبار الذي يحتوي على مكالمات إلى وظائف setTimeout أو setInterval في حد ذاته أو في التعليمات البرمجية المختبرة يتعطل . حول ما يجب فعله في هذه الحالة ، سنتحدث أكثر.


لماذا يحدث ذلك؟


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


ضع في اعتبارك مثالاً بسيطًا ، فإن رمز الاختبار هو أسلوب فئة يغير حالة المثيل بشكل غير متزامن. وهي ليست مكتوبة بشكل مثالي:


export class Foo {
  propToTest: boolean = false;
  methodToTest(): void {
    setTimeout(() => {
      this.propToTest = true;
    }, 100);
  }
}

? , setTimeout . c , (, 101 100 ). :


it('should set propToTest to true', (done: Function) => {
  const inst = new Foo();
  inst.methodToTest();
  setTimeout(() => {
    assert.isTrue(inst.propToTest);
    done();
  }, 101);
});

? , , .. , callback, setTimeout, . — , 2 , . , . -, . , , - , — -.
setInterval.



, . , . , , .


  1. callback- , setTimeout/setInterval (" 1001 101, "). .
  2. setTimeout() (" event loop, "). .
  3. ( 1 2) — , , .

?


, , . , , : . .


  1. — .. .
    , , :


    export function setProperState(inst: Foo) {
      inst.propToTest = true;
    }
    
    export class Foo {
      propToTest: boolean = false;
      methodToTest(): void {
        setTimeout(() => {
          setProperState(this);
        }, 100);
      }
    }

    :


    it('should set propToTest to true', () => {
      const inst = new Foo();
      assert.isFalse(inst.propToTest);
      setProperState(inst);
      assert.isTrue(inst.propToTest);
    });

    , setProperState methodToTest ( ). , : , . , , .


  2. , , .
    , setTimeout :


    export class Foo {
      propToTest: boolean = false;
      constructor(readonly awaiter: Function = setTimeout) {
      }
      methodToTest(): void {
        this.awaiter(() => {
          this.propToTest = true;
        }, 100);
      }
    }

    -, :


    it('should set propToTest to true', () => {
      const awaiter = (callback) => callback();
      const inst = new Foo(awaiter);
      inst.methodToTest();
      assert.isTrue(inst.propToTest);
    });

  3. , Promise.
    , — , . . . :


    export class Foo {
      propToTest: boolean = false;
      methodToTest(): Promise<void> {
        return new Promise((resolve) => {
          setTimeout(() => {
            this.propToTest = true;
            resolve();
          }, 100);
         });
      }
    }

    setTimeout:


    it('should set propToTest to true', () => {
      const inst = newFoo();
      return inst.methodToTest().then(() => {
        assert.isTrue(inst.propToTest);
      });
    });

    , , , " ". setTimeout , .


    export class Foo {
      propToTest: boolean = false;
      constructor(readonly asyncTimeout: number = 100) {
      }
      methodToTest(): void {
        return new Promise((resolve) => {
          setTimeout(() => {
            this.propToTest = true;
            resolve();
          }, this.asyncTimeout);
         });
      }
    }

    .


  4. fake timers.
    Sinon.JS , . - ( ), setTimeout .
    :


    export class Foo {
      propToTest: boolean = false;
      methodToTest(): void {
        setTimeout(() => {
          this.propToTest = true;
        }, 100);
      }
    }

    fake timer, (!):


    let clock;
    
    beforeEach(() => {
      clock = sinon.useFakeTimers();
    });
    
    afterEach(() => {
      clock.restore();
    });
    
    it('should set propToTest to true', () => {
      const inst = newFoo();
      inst.methodToTest();
      clock.tick(101);
      assert.isTrue(inst.propToTest);
    });

    " " clock.restore(), .




- , . - , . , , .


All Articles