7

I am using @ngrx/effects 4.1.1. I have an effect that returns an empty observable like this:

@Effect() showDialog$: Observable<Action> = this
.actions$
.ofType( ActionTypes.DIALOG_SHOW )
.map( ( action: DialogAction ) => action.payload )
.switchMap( payload => {
    this.dialogsService.showDialog( payload.className );
    return empty();
} );

I am trying to write a unit test following these guidelines that will test that the effect yields an empty observable. I have this:

describe( 'DialogEffects', () => {
    let effects: DialogEffects;
    let actions: Observable<any>;
    const mockDialogService = {
        showDialog: sinon.stub()
    };

    beforeEach( () => {
        TestBed.configureTestingModule( {
            providers: [
                DialogEffects, provideMockActions( () => actions ),
                {
                    provide: DialogsService,
                    useValue: mockDialogService
                }
            ]
        } );

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

    describe( 'showDialog$', () => {
        it( 'should return an empty observable', () => {
            const dialogName = 'testDialog';
            const action = showDialog( dialogName );

            actions = hot( '--a-', { a: action } );
            const expected = cold( '|' );

            expect( effects.showDialog$ ).toBeObservable( expected );
        } );
    } );
} );

However, Karma (v1.7.1) complains:

Expected [ ] to equal [ Object({ frame: 0, notification: Notification({ kind: 'C', value: undefined, error: undefined, hasValue: false }) }) ].

How do I test that the effect returns empty()? I have tried modifying the effect metadata using dispatch: false, but this has no effect.

Ideas?

serlingpa
  • 12,024
  • 24
  • 80
  • 130
  • Your effect won't complete, so your cold observable should be `cold('');` - the pipe character represents the observable stream's completion. – cartant Jan 07 '18 at 12:49
  • Thanks cartant, that works now! You should post it and I will mark is as solved. – serlingpa Jan 07 '18 at 13:03
  • I assumed that empty() would immediately complete. – serlingpa Jan 07 '18 at 13:05
  • `switchMap` sees the empty observable (yes, it does complete) merged into the effect obsevable. The effect obsevable does not complete. Basically, you never want to complete an effect observable. If an effect does complete, it stops responding to actions. – cartant Jan 07 '18 at 13:16
  • So if the empty observable, which does complete, is merged into the effect observable, why does the effect observable not complete when it comes across this empty observable's complete? – serlingpa Jan 07 '18 at 19:42
  • Because if the merge were to complete (i.e. stop merging) when it sees the completion of a merged observable, it wouldn't be a merge - pretty much by definition. – cartant Jan 07 '18 at 20:08

3 Answers3

11

The problem is that you are comparing the actual result against cold('|').

The pipe character in cold('|') represents the completion of the observable stream. However, your effect will not complete. The empty() observable does complete, but returning empty() from switchMap just sees that observable merged into the effect observable's stream - it does not complete the effect observable.

Instead, you should compare the actual result against cold('') - an observable that emits no values and does not complete.

cartant
  • 57,105
  • 17
  • 163
  • 197
6

The best way to do this is to use dispatch false in the effect and change the swicthMap for a tap

@Effect({dispatch: false}) showDialog$ = this.actions$.pipe(
   ofType( ActionTypes.DIALOG_SHOW ),
   map( ( action: DialogAction ) => action.payload ),
   tap( payload => 
      this.dialogsService.showDialog( payload.className )
    ));

To test it you can do like

describe( 'showDialog$', () => {
    it( 'should return an empty observable', () => {
        const dialogName = 'testDialog';
        const action = showDialog( dialogName );
        actions = hot( '--a-', { a: action } );

       expect(effects.showDialog$).toBeObservable(actions);
// here use mock framework to check the dialogService.showDialog is called
    } );
} );

Notice the reason I use toBeObservable(actions) thats because due to disptach false this effect will not produce anything so actions is the same as before , what you gain by calling toBeObservable(actions) is that is makes the test synchronous, so you can check after it that your service was called with a mock framework like jasmine or ts-mockito

  • Do you have an idea how to verify that `{ dispatch: false }` is present. The test in your answer would still pass when `{ dispatch: false }` is removed from the effect. – Wilgert Aug 27 '19 at 10:16
  • Yes for versions before angular 8 you have https://v7.ngrx.io/guide/effects/testing#geteffectsmetadata, to check that, but that only works for @Effect as far as I know , in 8 the recomended way is to use createEffect and not sure getEffectsMetadata works for it is omitted in the docs – Gabriel Guerrero Aug 28 '19 at 13:39
  • Fun fact: we are using Angular and NgRx 8... So the question remains, how do we test this? – Wilgert Aug 28 '19 at 14:14
  • Hey I just happen to test getEffectsMetadata with an effects in angular 8 using the createEffect and it still works, no idea why is not in the docs any more. – Gabriel Guerrero Aug 29 '19 at 09:18
  • We will try that as well then! Thank you for helping out! – Wilgert Aug 29 '19 at 09:19
2

To test that effect does not dispatch, you can assert against it's metadata with getEffectsMetadata function:

expect(getEffectsMetadata(effects).deleteSnapshotSuccess$?.dispatch).toBe(false)

The above has been tested with ngrx 9.0.0. Since all observables emit regardless of dispatch, checks like expect(effects.effect$).toBeObservable(cold('')) won't produce expected results.