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(), .
- , . - , . , , .