Testing NgRx Effects with Async/Await

This post explains a delightful approach to testing NgRx Effects that could help simplify your code.

tl;dr:

  • Test Effects as Observables , but use async / await and firstValueFrom instead of .subscribe().
  • Here is source code for a demo application with example specs.

1. Common Syntax for Testing Effects

The NgRx documentation explains a few ways to test Effects. To summarize, the approaches either use marble diagram syntax or use subscribe().

A. Marble Diagrams and Test Scheduler

Marble diagrams have such a steep learning curve that I’ve never worked with someone who adamantly recommends them. Test Scheduler also uses marble diagram syntax so I’ve generally avoided it for the same reason.

B. Testing Effects Using subscribe()

Whether you test your Effect with an Observable or a ReplaySubject, the NgRx docs provide examples using subscribe() to obtain the value returned from the tested effect.

C. Testing Effects using async / await Syntax

This post introduces a pattern for testing Effects with Observables using async / await instead of subscribe().

I personally find async / await easier to read and understand than using .subscribe(). There’s a simple pattern for arranging data, performing actions involving Effects, resolving those actions, and making assertions based on those results.

One added benefit of this approach is that you don’t need to worry about cleaning up subscriptions in your spec. firstValueFrom automatically unsubscribes for you.

2. The Demo Application, Jest, and Test Setup

In this section, I’ll review the setup for our tests. If you just want to see the testing code you can skip to the next section or jump to the widgets.effects.spec file in the example repository.

A. The init$ Effect

Our application has a home page that displays a list of widgets retrieved from a mocked server. We have an init$ Effect that is triggered whenever the initWidgets Action is dispatched from the home page. The init$ Effect will be the focus of our tests:

init$ = createEffect(() =>
  this.actions$.pipe(
    ofType(WidgetsActions.initWidgets),
    fetch({
      run: (action) =>
        this._widgetsService
          .all()
          .pipe(
            map((widgets: WidgetsEntity[]) =>
              WidgetsActions.loadWidgetsSuccess({ widgets })
            )
          ),
      onError: (action, error) => {
        return WidgetsActions.loadWidgetsFailure({ error });
      },
    })
  )
);

The init$ Effect If the http service successfully retrieves widgets, the init$ effect returns loadWidgetsSuccess. If the service returns an error, the effect returns loadWidgetsFailure.

B. Setting Up the Tests

Now that we’ve created an effect, we need to set up providers and spies in the beforeEach method:

describe('WidgetsEffects', () => {
  let actions: Observable<Action>;
  let effects: WidgetsEffects;
  let allWidgetsSpy: jest.Mock<any, any>;

  beforeEach(() => {
    allWidgetsSpy = jest.fn();

    TestBed.configureTestingModule({
      imports: [NxModule.forRoot()],
      providers: [
        WidgetsEffects,
        { provide: WidgetsDataService, useValue: { all: allWidgetsSpy } },
        provideMockActions(() => actions),
        provideMockStore(),
      ],
    });

    effects = TestBed.inject(WidgetsEffects);
  });

We know that init$ relies on a http service to get the widgets. However, in order to keep our tests thin we’ll avoid injecting the full service into TestBed. Instead, we create a spy using jest.fn() and use that spy in a mock provider defined simply as { all: allWidgetsSpy } }.

Our providers also include provideMockActions(() => actions). This provider allows us to effectively dispatch an Action in our tests and trigger the corresponding Effect.

3. Testing Using Await/Async

Finally, let’s write some specs. We’ll write one test for when we expect init$ to return loadWidgetsSuccess, and a second spec for loadWidgetsFailure.

A. The loadWidgetsSuccess Test

Here is the unit test for the init$ Effect that expects a return value of loadWidgetsSuccess:

it('should return loadWidgetsSuccess', async () => {
  // Arrange
  const widgets = [{ id: 1, name: 'Widget 01' }];
  allWidgetsSpy.mockReturnValue(of(widgets));
  const expected = WidgetsActions.loadWidgetsSuccess({ widgets });

  // Act
  actions = of(WidgetsActions.initWidgets());

  // Await
  const result = await firstValueFrom(effects.init$);

  // Assert
  expect(allWidgetsSpy).toBeCalled();
  expect(result).toEqual(expected);
});

This test is divided into four steps: Arrange, Act, Await, and Assert. Let’s review each step line-by-line.

Arrange: On line 1 we see that the spec’s callback is an async function. Lines 3 and 4 create a mock widget, and set the spy’s return value as an Observable containing the mocked widget. Line 5 defines the expected result of the init$ Effect in this test. Here, we expect the loadWidgetsSuccess action with a payload containing our mock widgets.

Act: Line 8 makes use of the provideMockActions provider, setting actions to the Action that triggers the init$ Effect.

Await: Line 11 is where we finally see our await. We grab the result of the init$ Effect by awaiting the firstValueFrom the init$ Effect. firstValueFrom is a rxjs operator introduced in Version 7 that converts and Observable source into a Promise. Once the Observable has been converted to a Promise we can easily await the result in our test.

Assert: Finally, Lines 14 and 15 contain our expects validating that our spy was called and our result equals what was expected.

B. The loadWidgetsFailure Test

For completeness’ sake, here is an example of a spec testing when the init$ method should return loadWidgetsFailure:

it('should return loadWidgetsFailure', async () => {
  // Arrange
  const error = 'Failed to load widgets';
  allWidgetsSpy.mockReturnValue(throwError(() => error));
  const expected = WidgetsActions.loadWidgetsFailure({ error });

  // Act
  actions = of(WidgetsActions.initWidgets());

  // Await
  const result = await firstValueFrom(effects.init$);

  // Assert
  expect(allWidgetsSpy).toBeCalled();
  expect(result).toEqual(expected);
});

4. Final Thoughts

Here are two quick tips about implementing the async / await approach.

A. Old RxJS Version

The rxjs operator firstValueFrom was released in RxJS version 7. If your project’s RxJS version is older than version 7 you’ll have to use the .toPromise() method instead. See the widgets.effects.spec in the demo repository for an example using .toPromise().

B. Does This Approach Work With Jasmine?

Yes! Although the example repository in this post uses Jest, you can still use the async / await testing syntax if your application uses Jasmine. You’ll just need to mock providers and spies without using jest.fn().

C. Conclusion

If you’ve made it this far thank you for reading this post! Thanks to my awesome colleague Rafael for introducing me to this testing syntax for Effects.