Comment tester le code contenant setTimeout / setInterval sous le capot

Nous, les développeurs, aimons beaucoup les tests unitaires, dont l'utilité est évidente. Et pour que ces tests soient vraiment utiles, et n'apportent pas de douleur, il faut s'assurer de leur stabilité.


Notre société développe le cadre d'interface Wasaby et vend des produits construits sur sa base, qui sont des applications cloud et de bureau. Le cycle de sortie est étroitement lié au calendrier et des processus d'intégration continue sont mis en place pour contrôler la qualité du produit. Nous utilisons Jenkins pour les assemblages et Mocha en conjonction avec Chai assertpour le test unitaire du code JavaScript. Et récemment, nous avons été confrontés à une situation où la surveillance de l'assemblage a commencé à montrer qu'environ la moitié de tous les cas de leur chute étaient dus à des tests unitaires JavaScript instables. Les symptômes sont les mêmes: un test distinct de l'ensemble n'a pas le temps de s'exécuter ou renvoie le mauvais résultat comme prévu. Et l'analyse des cas révèle presque toujours le fait qu'un test contenant des appels aux fonctions setTimeout ou setInterval en soi ou dans le code testé plante . Sur ce qu'il faut faire dans cette situation, nous parlerons plus loin.


Pourquoi ça arrive?


Tout est simple - à l'intérieur du code que le développeur teste, un appel asynchrone se produit, à la fin duquel l'état de l'environnement change en quelque sorte. Et le test est conçu pour vérifier que cette condition est conforme aux attentes. Mais depuis Comme nous ne vivons pas dans un monde idéal, il arrive qu'un test soit écrit pour du code qui fonctionne déjà au combat et qui n'a pas été initialement préparé pour ce type de test. Et le développeur doit décider comment il peut tester un tel code.


Prenons un exemple simple, le code testé est une méthode de classe qui modifie de manière asynchrone l'état d'une instance. Et ce n'est pas écrit parfaitement:


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