DEV Community

Cover image for Testing setTimeout/setInterval
bob.ts
bob.ts

Posted on • Edited on

Testing setTimeout/setInterval

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 = [];
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

And ... we can now use async/await ...

... from Asynchronous Work

This pattern actually means that setTimeout needs to be adjusted to allow it to resolve ...

var testableVariable = false;

const sleep = (time) => {
 return new Promise(resolve => setTimeout(resolve, time));
};

async function testableCode() {
  await sleep(10);
  testableVariable = true;
}
Enter fullscreen mode Exit fullscreen mode

Then, testing becomes ...

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

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

With this code, we can simply test the testableAfterTimeout function directly ...

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

setInterval

Looking at another example ...

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

    counter++;
  }, 500);

  return interval;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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){
    testableVariable2 = true;
    clearInterval(interval);
  }
  counter++;
}
function testableCode2() {
  counter = 1
  interval = setInterval(testableAfterInterval, 500);
  return interval;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (3)

Collapse
 
cellog profile image
Gregory Beaver

The mocking of timings provided natively by Jest is fantastic, by calling jest.useFakeTimers the timeout functions become mocks and you can advance "time" by a fake number of milliseconds or run all pending timers or all timers to check that behavior works as expected. Of course, there are hidden gotchas, as you'll want to reset all mocks after each test runs but mostly it is easier than custom rolled timeout handling. Not to take away from your work - what you designed is basically what jest provides internally

Collapse
 
rfornal profile image
bob.ts

Awesome! Good to know.

There’s a similar “time” pattern in Jasmine, I believe. The issue here was a client with an existing code-base, including thousands of tests already in Jasmine. In fact, they prefer to only upgrade 3rd party code when it’s clearly broken ... which explains the odd pattern we worked out in this article.

Thanks again for the information. I may jump in and experiment with Jest one of these days!

Collapse
 
asg5704 profile image
Alexander Garcia

I attempted to use the sinon.useFakeTimers and it was not working at all. I'm glad I found your article as it helped me to debug to see where exactly the code was breaking. Thank you for your article.