In my Angular 7 application, I have a service that is used to track active user tasks. In the service, a timer runs every second to check if any tasks still haven't been completed within 30 seconds. If any tasks are found to have expired, the task is emitted via an event emitter on the service to be handled elsewhere. This all works when the app is running in a browser, but when I try to write a unit test to test the behavior in a fakeAsync environment, tick(X)
does not advance the time (or fakeAsync is not mocking the time for any 'new Date()' created within the service for tick()
to work properly).
As I am new to angular unit testing, I also admit the issue could be how I am setting up the tests (in fact, I suspect this is the issue).
I have found a number of posts regarding older versions of Angular have had issues with Date not being mocked properly so the suggested workarounds were to use asyncScheduler to bypass tick or import other npm packages or, I have even tried other versions of the functions from the zone. I have tried these with no success. I have also tested the fakeAsync()
and tick()
functions from @angular/core/testing
by running the simple test below which passes:
it('should tick', fakeAsync(() => {
const firstDate = new Date();
tick(30000);
const secondDate = new Date();
expect(secondDate.getTime() - firstDate.getTime()).toBe(30000);
}));
Here is a simplified version of the service:
export class UserTaskTrackerService {
TaskExpired = new EventEmitter<UserTask>
private activeUserTasks: UserTask[] = []
private oneSecondTimer;
private timerSubscription$;
constructor() {
this.oneSecondTimer = timer(1000, 1000);
this.timerSubscription$ = this.oneSecondTimer.subscribe(() => {
this.checkForExpiredUserTasks();
});
}
addNewTask(task: UserTask) {
if(this.taskExists(task)) {
this.completeTask(task); // not included in example
}
else {
task.startTime = new Date();
this.activeUserTasks.push(task);
}
}
private checkForExpiredUserTasks() {
const currentTime = new Date();
const expiredTasks: UserTask[] = [];
this.activeUserTasks.forEach(userTask => {
if (this.taskHasExpired(userTask.startTime, currentTime)) {
expiredTasks.push(userTask);
}
});
if (expiredTasks.length > 0) {
this.handleExpiredTasks(expiredTasks);
}
}
private taskHasExpired(taskStartTime: Date, currentTime: Date): boolean {
return (currentTime.getTime() - taskStartTime.getTime()) / 1000 > 30;
}
private handleExpiredTasks(expiredTasks: UserTasks[]) {
// remove task(s) from collection and then emit the task
}
}
Example unit tests. In this example, all testing functions from from @angular/core/testing
describe('User Action Tracking Service', () => {
let service: UserTaskTrackerService;
let testBed: TestBed;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserTaskTrackerService]
});
});
beforeEach(() => {
service = TestBed.get(UserTaskTrackerService);
});
it('should tick', fakeAsync(() => {
const firstDate = new Date();
tick(30000);
const secondDate = new Date();
expect(secondDate.getTime() - firstDate.getTime()).toBe(30000);
}));
// Other tests removed for brevity
it(`it should emit a UserTask when one expires`, fakeAsync(() => {
let expiredUserTask: UserTask;
service.TaskExpired.subscribe((userTask: UserTask) => {
expiredUserTask = userTask;
});
service.addNewTask(new UserTask('abc', 'test action - request'));
expect(service.getTaskCount()).toBe(1);
tick(31000);
expect(expiredUserTask).toBeDefined();
expect(expiredUserTask.id).toBe('abc');
}));
});
When the test runs, I get a failed result saying "expected 'undefined' to be 'defined'. "
If I continue to watch the console, ~30 seconds after the testing finished, I see some console.log output which I have in my service code which prints the expired user task when an expired task is found.