Testing NGRX effects without testbed

A guide to writing NGRX effects unit tests without the need for Angular's Testbed.

blogpost logo image
NGRX Unit Tests Jest

The benefits

📍 The benefits

When testing NGRX effects the testbed is not needed. Dependencies can be mocked and directly passed to the constructor and actions can just be a simple observable stream of any form.

The testbed often creates additional bloat slowing down your unit tests and requiring more boilerplate to be written when mocking injection dependencies.

How do you mock actions without the testbed?

📍 How do you mock actions without the testbed?

It’s very simple. First remove the testbed configureTestingModule and inject code and replace with a raw constructor.

describe('ExampleEffects', () => {
    ...
    let effects: ExampleEffects;

    beforeEach(() => {
        effects = new ExampleEffects();
    });

    ...
});

Then we need to create a new observable stream for our action as we can’t use ngrx’z included provideMockActions anymore. I like to use a replay subject for this as it allows for jest-marbles tests to run successfully.

describe('ExampleEffects', () => {
    ...
    let effects: ExampleEffects;
    let actions$: ReplaySubject<Action>;

    beforeEach(() => {
        actions$ = new ReplaySubject<Action>();
        effects = new ExampleEffects(actions$);
    });

    ...
});

Now to dispatch actions all we need to do is call next on our replay subject with the relevant action.

Let’s see this in action. Below is an effect file which contains an effect that listens for actionA and then spits out actionB.

export class ExampleEffects {
    exampleEffect$ = createEffect(() =>
        this.actions$.pipe(
            ofType(ExampleActions.actionA),
            map(() => ExampleActions.actionB())
        )
    );

    constructor(private actions$: Actions) {}
}

And below is my spec file.

describe('ExampleEffects', () => {
    let effects: ExampleEffects;
    let actions$: ReplaySubject<Action>;

    beforeEach(() => {
        actions$ = new ReplaySubject<Action>();
        effects = new ExampleEffects(actions$);
    });

    it('should be created', () => {
        expect(effects).toBeTruthy();
    });

    describe('exampleEffect$', () => {
        beforeEach(() => actions$.next(ExampleActions.actionA()));

        it('should return the deleteUserData action', () => {
            const expected$ = cold('a', { a: ExampleActions.actionB() });

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

Conclusion

📍 Conclusion

I hope someone finds this useful, removing the testbed is surprisingly simple for NGRX effects and you may notice considerable performance improvements in large effects files.

More Posts