Cara menguji kode yang mengandung setTimeout / setInterval di bawah tenda

Kami para pengembang sangat menyukai unit test, yang manfaatnya jelas. Dan agar tes ini benar-benar bermanfaat, dan tidak menimbulkan rasa sakit, perlu untuk memastikan stabilitasnya.


Perusahaan kami mengembangkan kerangka antarmuka Wasaby dan menjual produk yang dibangun atas dasar itu, yaitu aplikasi cloud dan desktop. Siklus rilis terkait erat dengan kalender, dan proses inegrasi terus menerus diatur untuk mengontrol kualitas produk. Kami menggunakan Jenkins untuk build dan Mocha bersama dengan Chai menegaskanuntuk unit yang menguji kode JavaScript. Dan baru-baru ini, kami dihadapkan pada situasi di mana pemantauan perakitan mulai menunjukkan bahwa sekitar setengah dari semua kasus kejatuhan mereka disebabkan oleh tes unit JavaScript yang tidak stabil. Gejalanya sama: tes terpisah dari set tidak memiliki waktu untuk berjalan, atau mengembalikan hasil yang salah seperti yang diharapkan. Dan analisis kasus hampir selalu mengungkapkan fakta bahwa tes yang berisi panggilan ke fungsi setTimeout atau setInterval sendiri atau dalam kode yang diuji macet . Tentang apa yang harus dilakukan dalam situasi ini, kami akan berbicara lebih lanjut.


Kenapa itu terjadi?


Semuanya sederhana - di dalam kode yang sedang diuji pengembang, panggilan asinkron terjadi, pada akhirnya kondisi lingkungan berubah. Dan tes ini dirancang untuk memverifikasi bahwa kondisi ini seperti yang diharapkan. Tapi sejak itu Karena kita tidak hidup di dunia yang ideal, kebetulan sebuah tes ditulis untuk kode yang sudah berfungsi dalam pertempuran, dan yang awalnya tidak disiapkan untuk pengujian semacam ini. Dan pengembang harus memutuskan bagaimana dia bisa menguji kode seperti itu.


Pertimbangkan contoh sederhana, kode yang sedang diuji adalah metode kelas yang secara asinkron mengubah keadaan instance. Dan itu tidak ditulis dengan sempurna:


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