5

My goal is to test API calls, taking delays into account. I was inspired by this post.

I've designed a sandbox in which a mock API takes 1000 msec to respond and change the value of a global variable result. The test checks the value after 500msec and after 1500msec.

Here is the code where the last test is supposed to fail:

    let result: number;
    
    const mockAPICall = (delay: number): Observable<number> => {
        console.log('API called');
        return Observable.of(5).delay(delay);
    };
    
    beforeEach(() => {
        console.log('before each');
    });
    
    it('time test', async(() => {
        result = 0;
        const delay = 1000;
        console.log('start');

        mockAPICall(delay).subscribe((apiResult: number) => {
            console.log('obs done');
            result = apiResult;
        });

        console.log('first check');
        expect(result).toEqual(0);

        setTimeout(() => {
                console.log('second check');
                expect(result).toEqual(0);
            }, 500
        );

        setTimeout(() => {
                console.log('third check');
                expect(result).toEqual(0);
            }, 1500
        );
    }));

The last test does fail as expected and I get this in the logs:

before each
API called
first check
second check
obs done
third check

Now if I place an async() in the beforeEach():

    beforeEach(async(() => {
        console.log('async before each');
    }));

, the test passes and I only get this in the logs:

async before each
API called
first check

I didn't expect that. Why this behavior ? What happens behind the scenes ?

Note: I'll need this async() in beforeEach() in future tests because I'll use a testBed and compileComponents.

Community
  • 1
  • 1
ThCollignon
  • 976
  • 3
  • 14
  • 31

1 Answers1

3

Your problem stems from an unfortunate edge case using Zones during testing, and is outlined in the Angular documentation here.

Writing test functions with done(), is more cumbersome than async and fakeAsync. But it is occasionally necessary. For example, you can't call async or fakeAsync when testing code that involves the intervalTimer() or the RxJS delay() operator.

This has to do with the implementation of timers in rxjs, and there's lots of good materials out there that can help you use TestSchedulers to test rxjs code that uses some of these operators (like delay).

For your case, you can choose to refactor your test to not use the delay operator, or you can fall back on done(), provided by Jasmine.

let result: number;

const mockAPICall = (delay: number): Observable<number> => {
    console.log('API called');
    return Observable.of(0).delay(delay); // changed from 5 -> 0, to make tests pass
};

beforeEach(async(() => {
    console.log('async before each');
}));

it('time test', done => async(() => {
    result = 0;
    const delay = 1000;
    console.log('start');

    mockAPICall(delay).subscribe((apiResult: number) => {
        console.log('obs done');
        result = apiResult;
    });

    console.log('first check');
    expect(result).toEqual(0);

    setTimeout(() => {
            console.log('second check');
            expect(result).toEqual(0);
        }, 500
    );

    setTimeout(() => {
            console.log('third check');
            expect(result).toEqual(0);
            done(); // indicate that the test is complete
        }, 1500
    );
})());

Because there is a problem using delay with async, Jasmine is "ending" the test early - you don't see failures, but you also won't see some of the logging statements.

xtianjohns
  • 706
  • 3
  • 12