So testen Sie Code mit setTimeout / setInterval unter der Haube

Wir Entwickler lieben Unit-Tests sehr, deren Nützlichkeit offensichtlich ist. Und damit diese Tests wirklich nützlich sind und keine Schmerzen verursachen, ist es notwendig, ihre Stabilität sicherzustellen.


Unser Unternehmen entwickelt das " Wasaby " -Schnittstellen-Framework und verkauft darauf basierende Produkte, bei denen es sich um Cloud- und Desktop-Anwendungen handelt. Der Freigabezyklus ist eng mit dem Kalender verbunden, und es werden kontinuierliche Integrationsprozesse eingerichtet, um die Qualität des Produkts zu kontrollieren. Wir verwenden Jenkins für Builds und Mocha in Verbindung mit Chai Assertzum Testen von JavaScript-Code. Und vor kurzem waren wir mit einer Situation konfrontiert, in der die Überwachung der Baugruppen zu zeigen begann, dass etwa die Hälfte aller Fälle ihres Sturzes auf instabile JavaScript-Komponententests zurückzuführen waren. Die Symptome sind dieselben: Ein vom Set getrennter Test hat entweder keine Zeit zum Ausführen oder gibt erwartungsgemäß das falsche Ergebnis zurück. Und die Analyse von Fällen zeigt fast immer, dass ein Test, der Aufrufe der Funktionen setTimeout oder setInterval für sich oder im getesteten Code enthält, abstürzt . Über das, was in dieser Situation zu tun ist, werden wir weiter sprechen.


Warum passiert es?


Alles ist einfach - innerhalb des Codes, den der Entwickler testet, tritt ein asynchroner Aufruf auf, an dessen Ende sich der Zustand der Umgebung irgendwie ändert. Der Test soll sicherstellen, dass dieser Zustand wie erwartet ist. Aber seit Da wir nicht in einer idealen Welt leben, wird ein Test für Code geschrieben, der bereits im Kampf funktioniert und der ursprünglich nicht für diese Art von Tests vorbereitet wurde. Und der Entwickler muss entscheiden, wie er einen solchen Code testen kann.


Betrachten Sie ein einfaches Beispiel: Der Testcode ist eine Klassenmethode, die den Status einer Instanz asynchron ändert. Und es ist nicht perfekt geschrieben:


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