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.
, . , . , , .
- callback- , setTimeout/setInterval (" 1001 101, "). .
- setTimeout() (" event loop, "). .
- ( 1 2) — , , .
?
, , . , , : . .
- — .. .
 , , :
 
 - 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 ( ). , : , . , , . 
 
- , , .
 , 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);
});
 
 
- , 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);
     });
  }
}
 
 - . 
 
- 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(), . 
 
- , . - , . , , .