Como testar o código que contém setTimeout / setInterval sob o capô

Nós desenvolvedores gostamos muito de testes de unidade, cuja utilidade é óbvia. E para que esses testes sejam realmente úteis e não tragam dor, é necessário garantir sua estabilidade.


Nossa empresa desenvolve a estrutura de interface Wasaby e vende produtos criados com base em eles, que são aplicativos em nuvem e desktop. O ciclo de liberação está intimamente ligado ao calendário e processos de integração contínua são configurados para controlar a qualidade do produto. Usamos Jenkins para montagens e Mocha em conjunto com Chai afirmapara testar o código JavaScript da unidade. Recentemente, fomos confrontados com uma situação em que o monitoramento de montagem começou a mostrar que cerca de metade de todos os casos de queda eram devidos a testes de unidade JavaScript instáveis. Os sintomas são os mesmos: um teste separado do conjunto não tem tempo para ser executado ou retorna o resultado errado conforme o esperado. E a análise de casos quase sempre revela o fato de que um teste que contém chamadas para o setTimeout ou setInterval funciona sozinho ou no código testado falha . Sobre o que fazer nessa situação, conversaremos mais.


Por que isso acontece?


Tudo é simples - dentro do código que o desenvolvedor está testando, ocorre uma chamada assíncrona, ao final da qual o estado do ambiente muda de alguma forma. E o teste foi projetado para verificar se essa condição é conforme o esperado. Mas desde Como não vivemos em um mundo ideal, acontece que um teste é escrito para código que já funciona em batalha e que não foi originalmente preparado para esse tipo de teste. E o desenvolvedor deve decidir como ele pode testar esse código.


Considere um exemplo simples: o código em teste é um método de classe que altera de forma assíncrona o estado de uma instância. E não está escrito perfeitamente:


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