paint-brush
Unit Testing Functionality: Testing setTimeout and setIntervalby@bob.js
6,491 reads
6,491 reads

Unit Testing Functionality: Testing setTimeout and setInterval

by Bob FornalOctober 17th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Pattern is a patch using a pattern I am somewhat uncomfortable with as a tester, but given the amount of code already in place, this seemed like a reasonable option. The pattern discussed would allow for both sets of functionality to be wrapped in such a way that they can be removed, as needed. This functionality allowed the IDs to be stored in a way they could be removed as the tests iterated. With this code, we can simply test the function inside the setTimeout function. The timer behavior could be mocked.

Company Mentioned

Mention Thumbnail
featured image - Unit Testing Functionality: Testing setTimeout and setInterval
Bob Fornal HackerNoon profile picture

Recently at a client, a question came up about unit testing functionality that used setTimeout and setInterval.

The issue in this particular case was that there were several additional locations where

setTimeout
and
setInterval
had been implemented ... and the complete codebase needs to run before testing. Because all code runs, there are "bleed over" cases where other code is interfering with the tests.

The Pattern

The pattern discussed would allow for both sets of functionality to be wrapped in such a way that they can be removed, as needed. This functionality allowed the IDs to be stored in a way that they could be removed as the tests iterated.

This pattern is a patch using a pattern I am somewhat uncomfortable with as a tester, but given the amount of code already in place, this seemed like a reasonable option.

var timeoutIds = [];
var intervalIds = [];

var windowSetTimeout = window.setTimeout;
var windowSetInterval = window.setInterval;

window.setTimeout = function testSetTimeout() {
  var id = windowSetTimeout.apply(this, arguments)
  timeoutIds.push(id);
  return id;
};

window.setInterval = function testSetInterval() {
  var id = windowSetInterval.apply(this, arguments)
  intervalIds.push(id);
  return id;
};

afterEach(function() {
  timeoutIds.forEach(window.clearTimeout);
  intervalIds.forEach(window.clearInterval);
  timeoutIds = [];
  intervalIds = [];
});

setTimeout

Now, having shown this pattern, I found a few other options that, while they seemed more reasonable, did not fit well with this established codebase.

The following examples were primarily derived from How to test a function which has a setTimeout with jasmine?

Part of the issue I see with these examples is that

setInterval
is not covered.

Given a function with a timeout inside:

var testableVariable = false;
function testableCode() {
  setTimeout(function() {
    testableVariable = true;
  }, 10);
}

Use done as a means to tell the test that the

expect
will be checked Asynchronously, allowing enough time to expire for the setTimeout in the code above to run ...

it('expects testableVariable to become true', function(done) {
  testableCode();

  setTimeout(function() {
    expect(testableVariable).toEqual(true);
    done();
  }, 20);
});

Additionally, the timer behavior could be mocked - this method allows jasmine to step the time forward.

it('expects testableVariable to become true', function() {
  jasmine.clock().install();

  testableCode();
  jasmine.clock().tick(10);

  expect(testableVariable).toEqual(true);
  jasmine.clock().uninstall();
});

And, we can now use async/await from Asynchronous Work

it('expects testableVariable to become true', async function() {
  await testableCode();
  expect(testableVariable).toEqual(true);
});

Also, the original code could be refactored to take the function inside the

setTimeout
out in a way to make it testable.

var testableVariable = false;
function testableAfterTimeout() {
  testableVariable = true;
}
function testableCode() {
  setTimeout(testableAfterTimeout, 10);
}

With this code, we can simply test the

testableAfterTimeout
function directly.

it('expects testableVariable to become true', function() {
  testableAfterTimeout();
  expect(testableVariable).toEqual(true);
});

setInterval

Looking at another example:

var testableVariable2 = false;
function testableCode2(){
  var counter = 1;
  var interval = setInterval(function (){
    if (counter === 5){
      testableVariable = true;
      clearInterval(interval);
    }

    counter++;
  }, 500);

  return interval;
}

In this case, we should be able to see that the previous test patterns should work in our favor here.

Use done as a means to tell the test that the

expect 
will be checked Asynchronously, allowing enough time to expire for the setTimeout in the code above to run.

it('expects testableVariable2 to become true', function(done) {
  testableCode2();

  setTimeout(function() {
    expect(testableVariable2).toEqual(true);
    done();
  }, 4000);
});

Additionally, the timer behavior could be mocked - this method allows jasmine to step the time forward.

it('expects testableVariable2 to become true', function() {
  jasmine.clock().install();

  testableCode2();
  jasmine.clock().tick(4000);

  expect(testableVariable2).toEqual(true);
  jasmine.clock().uninstall();
});

Also, the original code could be refactored to take the function inside the

setInterval
out in a way to make it testable.

var testableVariable2 = false;
var counter = 1;
var interval;
function testableAfterInterval() {
  if (counter === 5){
    testableVariable = true;
    clearInterval(interval);
  }
  counter++;
}
function testableCode2() {
  counter = 1
  interval = setInterval(testableAfterInterval, 500);
  return interval;
}

With this code, we can simply test the

testableAfterInterval
function directly.

it('expects testableVariable2 to become true', function() {
  counter = 5;
  testableAfterInterval();
  expect(testableVariable2).toEqual(true);
});

Conclusion

There are many means of handling Asynchronous behavior. I've only mentioned a few here (and in a specific test framework). These are simply a few patterns that can be used when these cases come up.

Also published at https://dev.to/rfornal/testing-settimeout-setinterval-22ff