Cómo probar el código que contiene setTimeout / setInterval debajo del capó

Los desarrolladores somos muy aficionados a las pruebas unitarias, cuya utilidad es obvia. Y para que estas pruebas sean realmente útiles y no traigan dolor, es necesario garantizar su estabilidad.


Nuestra empresa desarrolla el marco de interfaz Wasaby y vende productos creados sobre la base de estos, que son aplicaciones en la nube y de escritorio. El ciclo de lanzamiento está estrechamente vinculado al calendario, y se configuran procesos continuos de integración para controlar la calidad del producto. Usamos Jenkins para ensamblajes y Mocha junto con la afirmación de Chaipara la prueba unitaria del código JavaScript. Y recientemente, nos enfrentamos a una situación en la que el monitoreo del ensamblaje comenzó a mostrar que aproximadamente la mitad de todos los casos de su caída se debieron a pruebas unitarias de JavaScript inestables. Los síntomas son los mismos: una prueba separada del conjunto no tiene tiempo para ejecutarse o devuelve el resultado incorrecto como se esperaba. Y el análisis de casos casi siempre revela el hecho de que una prueba que contiene llamadas a setTimeout o setInterval funciona por sí sola o en el código probado se bloquea . Sobre qué hacer en esta situación, hablaremos más.


¿Por qué sucede?


Todo es simple: dentro del código que el desarrollador está probando, se produce una llamada asincrónica, al final de la cual el estado del entorno cambia de alguna manera. Y la prueba está diseñada para verificar que esta condición sea la esperada. Pero desde Como no vivimos en un mundo ideal, sucede que se escribe una prueba para el código que ya funciona en la batalla, y que originalmente no estaba preparado para este tipo de prueba. Y el desarrollador debe decidir cómo puede probar dicho código.


Considere un ejemplo simple, el código bajo prueba es un método de clase que cambia asíncronamente el estado de una instancia. Y no está escrito perfectamente:


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