简体   繁体   中英

How do I test an angular service that uses setInterval using jasmine?

I am attempting to setup a test of a setInterval call. setInterval always creates a new timer so no matter how far ahead you move tick() there will always be an error with this message:

Error: 1 timer(s) still in the queue.

If I override the setTimeout function with setInterval, then the test passes as expected. I have added code to allow this to my angular service using a public property myTimerFunc that is normally set to setInterval. The test code sets it to setTimeout.

Is there a better way to test setInterval?

It would be nice if the last timer in the queue would be ignored by jasmine. Then I could test without extra cruft.

For example this code will fail unless you uncomment the line that overrides setInterval. I do not mind doing this in the test code but I do want to avoid adding extra stuff to the normal code just to allow this override.

 import { fakeAsync, tick } from '@angular/core/testing' describe('testing setTimeout and setInterval:', () => { it('setTimeout should work', fakeAsync(() => { let callback = jasmine.createSpy('setTimeoutCallback') setTimeout(() => { callback() }, 55) tick(56) expect(callback).toHaveBeenCalled(); })); // let setInterval = setTimeout it('setInterval should work', fakeAsync(() => { let callback = jasmine.createSpy('setTimeoutCallback') setInterval(() => { callback() }, 55) tick(56) expect(callback).toHaveBeenCalled(); })); }) 

If you wrapped the setInterval in a service, you could mock the service and everything would be deterministic, with no side effects. Basically you'd just do something like

class IntervalService {

  setInterval(callback: () => void, interval: number) {
    return setInterval(callback, interval);
  }

  clearInterval(intervalId: number) {
    clearInterval(intervalId);
  }
}

The way you could mock it is by doing something like

class MockIntervalService {
  callback;

  setInterval(callback: () => any, interval: number) {
    this.callback = callback;
    return 1;
  }

  clearInterval(interval: number) {
    callback = undefined;
  }

  tick() {
    callback();
  }
}

Then in your tests, you can control intervals by calling tick on the mock service. The best part is that everything is synchronous, making the tests easier to reason about.

It's a little extra work to inject the service, but I don't think it's that much of a burden.

Some theory about FakeAsync, flush: which will help you to understand the problem

By using fakeAsync we can ensure that all of our asynchronous code has run before we make assertions in our tests, and we even have fine-tuned control over how we want to advance time throughout the test.

  • When a test is running within a fakeAsync zone, we can use two functions called flushMicrotasks and tick. The tick function will advance time by a specified number of milliseconds, so tick(100) would execute any asynchronous tasks that would occur within 100ms. The flushMicrotasks function will clear any microtasks that are currently in the queue. In short, if you are not sure about time or dependent on promise/settimeout then you need to use flush() method.

Simplifying your question for understanding about the answer:

it('should test some async code', fakeAsync(() => {
    let flag = false;
    setTimeout(() => { flag = true; }, 100);
    expect(flag).toBe(false); // PASSES
    tick(50);
    expect(flag).toBe(false); // PASSES
    tick(50);
    expect(flag).toBe(true); // PASSES
}));

Other possible alternative:

it('should test some asynccode', fakeAsync(() => {
    let flag = false;

    setTimeout(() => { flag = true; }, 100);

    expect(flag).toBe(false);

    flushMicrotasks();

    expect(flag).toBe(true); // FAILS
}));

In your scenario,

flush() will work

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM