Platform: Calling done() in tests does not work correctly when subscribing to effects

Created on 5 Feb 2019  ·  3Comments  ·  Source: ngrx/platform

Minimal reproduction of the bug/regression with instructions:

Jest and Jasmine examples found in this repo
Run npm install and npm test in each folder. The test is found in src/app/example.spec.ts

Invoking the async callback in a Jasmine or Jest test does not appear to work if you do it inside the subscribe block of an effect.

E.g. in the code below

import { Action } from '@ngrx/store';
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { map } from 'rxjs/operators';
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable } from 'rxjs';
import { hot } from 'jasmine-marbles';

const EXAMPLE = 'Example';
class ExampleAction implements Action {
    readonly type = EXAMPLE;
    constructor() { }
}

@Injectable()
class ExampleEffects {
    constructor(private actions$: Actions) { }

    @Effect()
    go = this.actions$.pipe(
        ofType(EXAMPLE),
        map(() => true),
    );
}

describe('Example', () => {
    let effects: ExampleEffects;
    let actions: Observable<any>;

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [
            ],
            providers: [
                ExampleEffects,
                provideMockActions(() => actions),
            ],
        });

        effects = TestBed.get(ExampleEffects);
    });

    it('it should work', (done) => {
        actions = hot('-a-|', { a: new ExampleAction() });

        effects.go.subscribe(() => {
            console.log('before done');
            done();
            console.log('after done');
        });
    });
});

For jasmine/karma we see the following output:

LOG: 'before done'
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1 SUCCESS (0 secs / 0 secs)
LOG: 'after done'
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 0 of 1 SUCCESS (0 secs / 0 secs)
HeadlessChrome 72.0.3626 (Windows 10.0.0) Example it should work FAILED
        Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
HeadlessChrome 72.0.3626 (Windows 10.0.0): Executed 1 of 1 (1 FAILED) (0 secs / 5.035 secs)

and for Jest we see:

  ● Example › it should work

    Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.

      41 |     });
      42 |
    > 43 |     it.only('it should work', (done) => {
         |        ^
      44 |         actions = hot('-a-|', { a: new ExampleAction() });
      45 |
      46 |         effects.go.subscribe(() => {

      at Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:85:20)
      at src/app/example.spec.ts:43:8
      at Object.<anonymous> (src/app/example.spec.ts:26:1)

  console.log src/app/example.spec.ts:47
    before done

  console.log src/app/example.spec.ts:49
    after done

In both cases after done is logged to the console so done() should have been called.

Expected behavior:

The above test should pass.

Versions of NgRx, Angular, Node, affected browser(s) and operating system(s):

NgRx 7.2.0
rxjs 6.3.3
Angular 7.2.0

Other information:

This appears to be a problem with effects specifically, regular observables behave as expected - the following test works

    it('it should work', (done) => {
        of({}).subscribe(() => {
            console.log('before done');
            done();
            console.log('after done');
        });
    });

I would be willing to submit a PR to fix this issue

[ ] Yes (Assistance is provided if you need help submitting a pull request)
[X ] No

Most helpful comment

This is because jasmine-marbles is using the TestScheduler (which will create a queue of actions) , to fire the actions you'll have to flush the actions:

actions = hot('-a--|', { a: new ExampleAction() });
    const scheduler = getTestScheduler();
    effects.go.subscribe(() => {
      console.log('beforeX done');
      done();
      console.log('after done');
    });
    scheduler.flush();

The RxJS version of this would be the following:

 actions = of(new ExampleAction(), getTestScheduler());
    const scheduler = getTestScheduler();
    effects.go.subscribe(() => {
      console.log('beforeX done');
      done();
      console.log('after done');
    });
    scheduler.flush();

All 3 comments

I am seeing this behavior as well

I believe this is because you're using the marbles syntax, the following does work.

actions = new ReplaySubject(1);
actions.next(new ExampleAction());

effects.go.subscribe(x => {
  done();
});

I'm not 100% sure, but I think if you're using the marble syntax you should also use .toBeObservable() to test the stream - expect(effects.someSource$).toBeObservable(expected);.

See the docs for more info.

This is because jasmine-marbles is using the TestScheduler (which will create a queue of actions) , to fire the actions you'll have to flush the actions:

actions = hot('-a--|', { a: new ExampleAction() });
    const scheduler = getTestScheduler();
    effects.go.subscribe(() => {
      console.log('beforeX done');
      done();
      console.log('after done');
    });
    scheduler.flush();

The RxJS version of this would be the following:

 actions = of(new ExampleAction(), getTestScheduler());
    const scheduler = getTestScheduler();
    effects.go.subscribe(() => {
      console.log('beforeX done');
      done();
      console.log('after done');
    });
    scheduler.flush();
Was this page helpful?
0 / 5 - 0 ratings