Como criar um teste unitário envolvendo timers | Angular
Aprenda a criar testes unitários para componentes Angular que utilizam setTimeout, setInterval, interval ou timer de forma eficiente e controlada com Jasmine e Jest.

Criar testes unitários eficazes é crucial para garantir a robustez e confiabilidade de um código. Por aqui já abordamos alguns exemplos de como construir testes unitários envolvendo alguns componentes, mas ainda não mostrei um exemplo envolvendo temporizadores, como o setTimeout
, setInterval
ou timer
e interval
do RxJS.
Para construir este exemplo, utilizei um projeto que usa a versão 16 do Angular, mas a lógica empregada é a mesma para outras versões. Deixei o link do repositório no final do texto.
O componente com o Timer
Para criar o cenário de estudo, construí um componente bem simples, similar a um cronômetro:

O código fonte do componente ficou da seguinte forma:
<div class="timer">
Timer: {{ tempoDecorridoEmSegundos }}s
</div>
<div>
<button (click)="iniciarTimer()">Iniciar</button>
<button (click)="pararTimer()">Parar</button>
</div>
timer.component.html
import { Component, OnDestroy } from '@angular/core';
import { EMPTY, Subscription, interval } from 'rxjs';
@Component({
selector: 'app-timer',
templateUrl: './timer.component.html',
})
export class TimerComponent implements OnDestroy {
tempoDecorridoEmSegundos: number = 0;
dataInicioTimer: Date = new Date();
subscriptionTimer: Subscription = EMPTY.subscribe();
ngOnDestroy(): void {
this.pararTimer();
}
iniciarTimer() {
const umSegundoEmMS = 1e3;
this.dataInicioTimer = new Date();
this.tempoDecorridoEmSegundos = 0;
this.subscriptionTimer = interval(
umSegundoEmMS
).subscribe(() => this.atualizarTimer());
}
atualizarTimer() {
const dataAtual = new Date();
this.tempoDecorridoEmSegundos = Math.floor(
(dataAtual.getTime() -
this.dataInicioTimer.getTime()) /
1000
);
}
pararTimer() {
this.subscriptionTimer.unsubscribe();
}
}
timer.component.ts
Construção dos testes unitários
Para construir os testes unitários vamos utilizar Jasmine, mas vou comentar sobre o Jest que também possui recursos similares para este cenário envolvendo um interval
.
A estrutura básica do teste ficará da seguinte forma:
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { TimerComponent } from './timer.component';
describe('TimerComponent', () => {
let component: TimerComponent;
let fixture: ComponentFixture<TimerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TimerComponent],
});
fixture = TestBed.createComponent(TimerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
timer.component.spec.ts
O primeiro método a ser testado é o iniciarTimer
. Este método é bastante simples, atribuindo valores a duas variáveis e iniciando um intervalo. Após a execução, esperamos que, por exemplo, o método atualizarTimer
seja chamado três vezes após 3 segundos, pois configuramos o intervalo para executar a cada 1 segundo.
Para realizar esse teste, não é necessário aguardar os 3 segundos reais. Podemos implementar um "relógio" simulado ("mock"), que substitui o controle padrão de data e hora. Ao utilizar jasmine.clock().install()
, temos a capacidade de estabelecer a data atual, atribuindo-a ao valor de new Date()
, e ajustá-la, por exemplo, para 2020-01-01T12:00:00.000Z
. Posteriormente, podemos avançar no tempo nesse "relógio" simulado conforme necessário, utilizando o comando jasmine.clock().tick()
.
Para ficar mais claro, veja o exemplo abaixo onde estamos simulando um valor para o new Date()
:
const dataMock = new Date('2020-01-01T12:00:00.000Z');
jasmine.clock().install();
jasmine.clock().mockDate(dataMock);
console.log('[1]', (new Date()).toISOString());
// [1], 2020-01-01T12:00:00.000Z
jasmine.clock().tick(3000);
console.log('[2]', (new Date()).toISOString());
// [2], 2020-01-01T12:00:03.000Z
Agora que sabemos como controlar a linha do tempo durante os testes unitários, podemos escrever o seguinte teste unitário para o método iniciarTimer
:
// ...(imports)...
describe('TimerComponent', () => {
// ...(trecho ocultado)...
afterEach(() => {
jasmine.clock().uninstall();
});
it(`deve iniciar dataInicioTimer e tempoDecorridoEmSegundos
e chamar o atualizarTimerSpy a cada segundo`, () => {
// Criamos um "spy" para saber quantas vezes o método
// `atualizarTimer` foi chamado e simulamos seu
// retorno:
const atualizarTimerSpy = spyOn(
component,
'atualizarTimer'
);
// Valor da data corrente que usaremos como valor
// simulado, ou seja, dentro deste teste qualquer
// `new Date()` retornará o valor abaixo:
const dataMock = new Date('2020-01-01T12:00:00.000Z');
// No comando a seguir criamos um relógio mockado
// para o JavaScript, ou seja, ao invés dele pegar
// a data e hora do computador, ele vai pegar
// a data e hora que nós vamos informar, de forma
// simulada:
jasmine.clock().install();
// Definimos qual o valor da corrente, ou seja,
// sempre que algum trecho de código executar
// `new Date()` o valor abaixo será retornado:
jasmine.clock().mockDate(dataMock);
// Chama o método que está sendo testado:
component.iniciarTimer();
// Esperamos que as variáveis sejam inicializadas
// com os valores `0` e `new Date()` respectivamente
expect(component.tempoDecorridoEmSegundos).toBe(0);
expect(component.dataInicioTimer.getTime()).toBe(
dataMock.getTime()
);
// Avançamos 3 segundos na linha do tempo p/verificar
// se o método `atualizarTimer` foi chamado 3 vezes.
// Lembrando que o método deve ser chamado a cada
// 1 segundo:
jasmine.clock().tick(3000);
expect(atualizarTimerSpy).toHaveBeenCalledTimes(3);
});
});
timer.component.spec.ts
Na sequência vamos criar um teste para o método atualizarTimer
. Este método calcula a diferença de tempo em segundos entre a data corrente e a data que consta na variável dataInicioTimer
. Então podemos adicionar um valor conhecido em dataInicioTimer
e novamente simular a linha do tempo para verificar se o cálculo que o método está fazendo é correto.
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TimerComponent } from './timer.component';
describe('TimerComponent', () => {
// ...(trecho ocultado)...
it(`deve atualizar o tempoDecorridoEmSegundos quando
atualizarTimer() for chamado`, () => {
// Configuramos um valor conhecido para a variável
// `dataInicioTimer`:
component.dataInicioTimer = new Date(
'2020-01-01T12:00:00.000Z'
);
const mockNewDate = new Date(
component.dataInicioTimer.getTime()
);
// Configuramos a linha do tempo de modo que a
// data corrente seja exatamente igual a variável
// `component.dataInicioTimer`
jasmine.clock().install();
jasmine.clock().mockDate(mockNewDate);
// Avançamos alguns segundos na linha do tempo:
const avancaRelogioEmXSegundos = 10;
jasmine.clock().tick(avancaRelogioEmXSegundos * 1000);
// Ao chamar o `atualizarTimer`, esperamos que seja
// calculada a diferença de tempo entre
// `component.dataInicioTimer` e `new Date()`.
// Lembrando que `new Date()` é um valor conhecido,
// mockado logo acima.
component.atualizarTimer();
expect(component.tempoDecorridoEmSegundos).toBe(
avancaRelogioEmXSegundos
);
// Também testamos se o valor de
// `component.tempoDecorridoEmSegundos` está sendo
// atualizado na tela. Para isto chamamos o
// `detectChanges` para que o Angular atualize o HTML
fixture.detectChanges();
const divTimer = fixture.debugElement.query(
By.css('div.timer')
);
const divTimerElement =
divTimer.nativeElement as HTMLDivElement;
expect(divTimerElement.textContent).toContain(
`Timer: ${avancaRelogioEmXSegundos}s`
);
});
});
timer.component.spec.ts
JEST
Utilizando o Jest a lógica é a mesma, o que muda são os métodos que o Jest disponibiliza:
import {
ComponentFixture,
TestBed,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TimerComponent } from './timer.component';
describe('TimerComponent', () => {
let component: TimerComponent;
let fixture: ComponentFixture<TimerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TimerComponent],
});
fixture = TestBed.createComponent(TimerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it(`deve iniciar dataInicioTimer e tempoDecorridoEmSegundos
e chamar o atualizarTimerSpy a cada segundo`, () => {
// Criamos um "spy" para saber quantas vezes o método
// `atualizarTimer` foi chamado e simulamos seu
// retorno:
const atualizarTimerSpy = jest.spyOn(
component,
'atualizarTimer'
).mockImplementation(() => {});
// Valor da data corrente que usaremos como valor
// simulado, ou seja, dentro deste teste qualquer
// `new Date()` retornará o valor abaixo:
const dataMock = new Date('2020-01-01T12:00:00.000Z');
jest.useFakeTimers({
now: dataMock
});
// Chama o método que está sendo testado:
component.iniciarTimer();
// Esperamos que as variáveis sejam inicializadas
// com os valores `0` e `new Date()` respectivamente
expect(component.tempoDecorridoEmSegundos).toBe(0);
expect(component.dataInicioTimer.getTime()).toBe(
dataMock.getTime()
);
// Avançamos 3 segundos na linha do tempo p/verificar
// se o método `atualizarTimer` foi chamado 3 vezes.
// Lembrando que o método deve ser chamado a cada
// 1 segundo:
jest.advanceTimersByTime(3000);
expect(atualizarTimerSpy).toHaveBeenCalledTimes(3);
});
it(`deve atualizar o tempoDecorridoEmSegundos quando
atualizarTimer() for chamado`, () => {
// Configuramos um valor conhecido para a variável
// `dataInicioTimer`:
component.dataInicioTimer = new Date(
'2020-01-01T12:00:00.000Z'
);
const mockNewDate = new Date(
component.dataInicioTimer.getTime()
);
// Configuramos a linha do tempo de modo que a
// data corrente seja exatamente igual a variável
// `component.dataInicioTimer`
jest.useFakeTimers({ now: mockNewDate });
// Avançamos alguns segundos na linha do tempo:
const avancaRelogioEmXSegundos = 10;
jest.advanceTimersByTime(
avancaRelogioEmXSegundos * 1000);
// Ao chamar o `atualizarTimer`, esperamos que seja
// calculada a diferença de tempo entre
// `component.dataInicioTimer` e `new Date()`.
// Lembrando que `new Date()` é um valor conhecido,
// mockado logo acima.
component.atualizarTimer();
expect(component.tempoDecorridoEmSegundos).toBe(
avancaRelogioEmXSegundos
);
// Também testamos se o valor de
// `component.tempoDecorridoEmSegundos` está sendo
// atualizado na tela. Para isto chamamos o
// `detectChanges` para que o Angular atualize o HTML
fixture.detectChanges();
const divTimer = fixture.debugElement.query(
By.css('div.timer')
);
const divTimerElement =
divTimer.nativeElement as HTMLDivElement;
expect(divTimerElement.textContent).toContain(
`Timer: ${avancaRelogioEmXSegundos}s`
);
});
});
timer.component.spec.ts
Considerações
A possibilidade de simular e controlar o relógio do JavaScript, como no Jasmine ou Jest, proporciona uma abordagem valiosa para testes unitários mais eficientes e controlados. Essa prática é particularmente benéfica para avaliar funcionalidades que envolvem timeouts, intervals e operações dependentes do tempo, resultando em testes mais rápidos, confiáveis e independentes do ambiente externo.
Link do projeto utilizando Jasmine e Jest respectivamente:
Link com os exemplos utilizados neste texto com Jasmine
Link com os exemplos utilizados neste texto com Jest
Outros links:
