How to Test Five Common NgRx Effect Patterns
Tags: ngrx, typescript, angular, patterns
Published: 2020-05-08 13:08:37

- 0. [Test Harness Setup](#0-test-harness-setup)
- 1. [Non-Dispatching Tap Effect](#1-nondispatching-tap-effect)
- 2. [Dispatching SwitchMap Effect](#2-dispatching-switchmap-effect)
- 3. [Multi-Dispatch Effect](#3-multi-dispatch effect)
- 4. [Store Dependent Effect](#4-store-dependent-effect)
- 5. [Timed Dispatch Effect](#5-timed-dispatch-effect)

## ⚠️ Requirements

I'm going to assume that you know something about [Angular](https://angular.io/) development with [TypeScript](https://www.typescriptlang.org/) and at least a little about the [NgRx](https://ngrx.io/) library and the [Redux](https://redux.js.org) pattern. You might gain some insight from reading through these patterns if you're at the beginning of your journey with these technologies but I don't intend this to be introductory. 

I don't necessarily expect this article to be read from start to end. Consider it reference material, which is why I've linked the patterns at the start.

## 🤯 Introduction

I've been using [NgRx](https://ngrx.io/) with [Angular](https://angular.io) for a few years now and yet _still_ every time I come to testing effects, my mind will often go blank. It's not that tests for effects are particularly arcane, I think it comes down to [cognitive load](https://en.wikipedia.org/wiki/Cognitive_load) and the Redux-style pattern itself. We know that there's a limited amount of things we can process at any one time, and there is already so much going on in my head trying to manage [actions](https://ngrx.io/guide/store/actions), [reducers](https://ngrx.io/guide/store/reducers) and [selectors](https://ngrx.io/guide/store/selectors), not to mention the complexities of understanding [RxJS](https://rxjs-dev.firebaseapp.com/) pipes that trying to cram testing into my head on top of that just causes my brain to stall. 

One way I try to solve this problem is by having working template examples to hand.

---

## 📋 Copy/Paste Driven Development

A lot of people deride this kind of technique as programming without thinking, but you know what? I'm ok with that. I don't want to have to think about what I'm writing *all* the time. Sometimes, I know the overall shape of what I need to build. I know which pieces I need to put together, but faffing around with the intricacies of _how_ I do that can be a distraction.  

Think back to learning about the [ternary operator](https://en.wikipedia.org/wiki/%3F:) for example:

```ts
const x = y ? 1 : 0;
```

How long was it before that started to feel natural? (If it even does?) When I started programming, that felt like a little bit of extra complexity that I didn't need. I'd often have to look up how it was used elsewhere in the code to confirm that I was using it correctly!

Having reference code to hand that you _know_ functions correctly is extremely useful, and not just for the novice programmer. You can copy that code and _then_ start modifying it. You know that you're starting from correct behaviour and you don't have to question everything about how you're writing the code, just the pieces you're changing.  

This isn't a strategy that's going to work for everything, but I find that when using NgRx (and reactive programming in general) it can be extremely useful as you find yourself writing very similar code over and over again.  

---

If you want to refer to some working code while checking out these patterns, I created a workspace here: https://github.com/jdpearce/ngrx-effects-patterns

---

## 0. Test Harness Setup

The workspace I created uses [Jest](https://jestjs.io/) but you could just as easily use [Jasmine](https://jasmine.github.io/) for testing. Much of the code would be similar except for the spies. I also use [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles) for Observable testing in most cases, but I won't use any particularly complicated syntax, I use it in the most basic way possible where I can get away with it.

Most effects spec files will be initially set up as follows (imports are omitted for brevity) :

```ts
describe('ThingEffects', () => {
  let actions: Observable<any>;

  // These are the effects under test
  let effects: ThingEffects;
  let metadata: EffectsMetadata<ThingEffects>;

  // Additional providers - very basic effects tests may not even need these
  let service: ThingService;
  let store: MockStore<fromThings.ThingsPartialState>;

  beforeEach(async(() => {
    const initialState = {
      // You can provide entirely different initial state here
      // it is assumed that this one is imported from the reducer file
      [fromThings.THINGS_FEATURE_KEY]: fromThings.initialState,
    };

    TestBed.configureTestingModule({
      providers: [
        ThingEffects,
        ThingService,
        provideMockActions(() => actions))
        provideMockStore({ initialState: initialAppState }),
      ],
    });

    effects = TestBed.inject(ThingEffects);
    metadata = getEffectsMetadata(effects);
    service = TestBed.inject(ThingService);
    store = TestBed.inject(Store) as MockStore<fromThings.ThingsPartialState>;
  }));
});
```

This should look like a standard Angular test harness but without any component under test. [provideMockActions](https://ngrx.io/api/effects/testing/provideMockActions) and [provideMockStore](https://ngrx.io/api/store/testing/provideMockStore) are crucial for helping us test effects. It was truly the dark times before these existed.

---

## 1. Non-Dispatching Tap Effect

```ts
performThingAction$ = createEffect(
  () =>
    this.actions$.pipe(
      ofType(ThingActions.performThingAction),
      tap(() => this.thingService.performAction())
    ),
  { dispatch: false }
);
```
This is an effect that only does one thing. It calls a service when receiving a particular action. We use [tap](https://rxjs-dev.firebaseapp.com/api/operators/tap) here because we don't want to modify the stream in any way. We _could_ change the stream however we like because NgRx isn't going to pay attention to the output, but it's good practice to leave the stream alone unless we have some reason to change it.

### 1.1 Testing for non-dispatch

All effects have [metadata](https://ngrx.io/guide/effects/lifecycle#effect-metadata) attached and one of the pieces of metadata is whether or not we expect that effect to dispatch another action.

We can test this by looking at the metadata directly :

```ts
it('should not dispatch', () => {
  expect(metadata.performThingAction$).toEqual(
    expect.objectContaining({
      dispatch: false,
    })
  );
});
```

### 1.2 Testing the service call is made

```ts
it('should call the service', () => {
  // set up the initial action that triggers the effect
  const action = ThingActions.performThingAction();

  // spy on the service call
  // this makes sure we're not testing the service, just the effect
  jest.spyOn(service, 'performAction');

  // set up our action list
  actions = hot('a', { a: action });

  // check that the output of the effect is what we expect it to be
  // (by doing this we will trigger the service call)
  // Note that because we don't transform the stream in any way,
  // the output of the effect is the same as the input.
  expect(effects.performThingAction$).toBeObservable(cold('a', { a: action }));

  // check that the service was called
  expect(service.performAction).toHaveBeenCalled();
});
```

> ⚠️ NB - If we want to cover all the bases we might write another test to check that this effect only fires when `ThingActions.performThingAction()` is emitted. I don't tend to do this, as it's just testing whether NgRx `ofType` works and whether you've remembered to add it. However, if this is the kind of thing you find you or your team are missing often, feel free to add that test and bring the lack of it up in code reviews. 

---

## 2. Dispatching SwitchMap Effect

```ts
getThings$ = createEffect(() =>
  this.actions$.pipe(
    ofType(ThingActions.getThings),
    switchMap(() =>
      this.thingService.getThings().pipe(
        map((things) => ThingActions.getThingsSuccess({ things })),
        catchError((error) => of(ThingActions.getThingsFailure({ error })))
      )
    )
  )
);
```

If you've used NgRx before this may look extremely familiar. An action comes in which triggers something like an API call. This call will either succeed or fail and we dispatch a success or failure action as a result. In large NgRx codebases you might have this kind of effect all over the place.

> ⚠️ NB - Depending on what dispatches the initial `getThings` action, you _may_ want to use [concatMap](https://rxjs-dev.firebaseapp.com/api/operators/concatMap) instead here. This is something that would probably be more important for a call which updated the backend rather than simply fetched data.

### 2.1 Successful service call

```ts
it('should get the items and emit when the service call is successful', () => {
  // set up the initial action that triggers the effect
  const action = ThingActions.getThings();

  // set up our dummy list of things to return
  // (we could create real things here if necessary)
  const things = [];

  // spy on the service call and return our dummy list
  jest.spyOn(service, 'getThings').mockReturnValue(of(things));

  // set up our action list
  actions = hot('a', { a: action });

  // check that the observable output of the effect is what we expect it to be
  expect(effects.getThings$).toBeObservable(
    cold('a', { a: ThingActions.getThingsSuccess({ things }) })
  );
});
```

> ⚠️ NB - It doesn't actually matter whether we use `hot` or `cold` here. If more than one thing was going to subscribe to the effect it might actually make a difference, but that doesn't happen here. The difference between `hot` and `cold` observables can be extremely confusing and it may make no sense that within these tests it seems irrelevant, but sorting out that particular [Gordian knot](https://en.wikipedia.org/wiki/Gordian_Knot) is beyond the scope of this article. Just know that this code, for the moment at least, does the job. (I don't want to encourage cargo cult development though, so when you have bandwidth to dig into the difference, there's [a good article by Ben Lesh](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) that goes into detail)

### 2.2 Unsuccessful service call

```ts
it('should emit an error action when the service call is unsuccessful', () => {
  // set up the initial action that triggers the effect
  const action = ThingActions.getThings();

  const error = 'There was an error';

  // spy on the service call and return an error this time
  spyOn(service, 'getThings').and.returnValue(throwError(error));

  // set up our action list
  actions = hot('a', { a: action });

  // check that the output of the effect is what we expect it to be
  expect(effects.getThings$).toBeObservable(
    cold('a', { a: ThingActions.getThingsFailure({ error }) })
  );
});
```

This is pretty similar to the previous test except we've sneaked in usage of the [throwError function](https://rxjs-dev.firebaseapp.com/api/index/function/throwError). You can follow the link for more detail but all it does is create an observable that immediately emits an error notification, which is exactly what we want to mock as a return value from our `getThings` method.

---

## 3. Multi-Dispatch Effect

```ts
initialiseThing$ = createEffect(() =>
  this.actions$.pipe(
    ofType(ThingActions.initialisingAction),
    switchMap((_action) => this.thingService.getThings()),
    switchMap((things) => {
      const actions: Action[] = [];
      if (!!things) {
        actions.push(ThingActions.getThingsSuccess({ things }));
      }
      actions.push(ThingActions.initialiseComplete());
      return actions;
    })
  )
);
```

Sometimes you need to dispatch more than one action. Again the choice of `switchMap` or `concatMap` (or even `mergeMap`) is very much context dependent, the important thing here is that one action goes in and one or more come out.

> ⚠️ NB - This also illustrates a slightly magical feature of `switchMap` where if you return any array-like object from the projection, it will automagically treat it like an observable of the objects in the array. In this case it's like switching to an `Observable<Action>`. If you find this confusing (and I wouldn't blame you here) you can replace `return actions;` with `return from(actions);` instead (cf. [from](https://rxjs-dev.firebaseapp.com/api/index/function/from)).

### 3.1 Testing for multiple action output

```ts
it('should emit initialiseComplete & getThingsSuccess if thing is found.', () => {
  const things = [
    {
      id: '1',
      name: 'Thing 1',
    },
  ];
  jest.spyOn(service, 'getThings').mockReturnValue(of(things));

  actions = hot('a', { a: ThingActions.initialisingAction() });
  const expected = cold('(bc)', {
    b: ThingActions.getThingsSuccess({ things }),
    c: ThingActions.initialiseComplete(),
  });

  expect(effects.initialiseThing$).toBeObservable(expected);
});
```

This shows the usage of a [sync grouping](https://rxjs-dev.firebaseapp.com/guide/testing/internal-marble-tests). That is, groups of notifications which are all emitted together. In this case, our `getThingsSuccess` and `initialiseComplete`. I've used this kind of pattern before to end an initialisation sequence of actions without making the last action do double-duty. Being able to fork your actions like this can be extremely useful if you have main sequences of actions with optional side quests being triggered (that's how I think of them).

### 3.2 Testing single action output

```ts
it('should just emit initialiseComplete if no things are found.', () => {
  const things = [];
  jest.spyOn(service, 'getThings').mockReturnValue(of(things));

  actions = hot('a', { a: ThingActions.initialisingAction() });
  const expected = cold('a', { a: ThingActions.initialiseComplete() });

  expect(effects.initialiseThing$).toBeObservable(expected);
});
```

This should look familiar. There's nothing new introduced here at all! Yay!

---

## 4. Store Dependent Effect

```ts
storeReadingEffect$ = createEffect(
  () =>
    this.actions$.pipe(
      ofType(ThingActions.thingsModified),
      withLatestFrom(this.store.pipe(select(selectThings))),
      map(([_action, things]) => this.thingService.persistThings(things))
    ),
  { dispatch: false }
);
```

Sometimes you end up needing to pull a value from the store. Don't feel bad about that. It's actually extremely common! In this case we're using [withLatestFrom](https://rxjs-dev.firebaseapp.com/api/operators/withLatestFrom) which means that every time we get a `thingsModified` action, we grab the latest state and `selectThings` from it. To test this, we need to provide some state and that's where `provideMockStore` and the [MockStore](https://ngrx.io/api/store/testing/MockStore) come into play.

```ts
it('should read things from the store and do something with them', () => {
  const things = [
    {
      id: '1',
      name: 'Thing 1',
    },
  ];

  // Note here that we have to provide a ThingsPartialState
  // not just a ThingsState.
  store.setState({
    [fromThings.THINGS_FEATURE_KEY]: {
      log: [],
      things,
    },
  });

  jest.spyOn(service, 'persistThings').mockReturnValue(of(things));

  actions = hot('a', { a: ThingActions.thingsModified() });

  expect(effects.storeReadingEffect$).toBeObservable(cold('a', { a: things }));

  expect(service.persistThings).toHaveBeenCalledWith(things);
});
```

The only new thing here is that we call `store.setState`. This is a wonderous boon to the test writing developer. In the old times we would actually dispatch actions to build up store state, but that would require those actions and associated reducers already existed and you would end up tightly coupling your tests to unrelated code. This is much simpler and neater (and it means you can write tests when the actions and reducers that might populate that slice of the store don't even exist yet).

---

## 5. Timed Dispatch Effect

```ts
timedDispatchEffect$ = createEffect(() =>
  this.actions$.pipe(
    ofType(ThingActions.startThingTimer),
    delay(ThingsEffects.timerDuration),
    mapTo(ThingActions.thingTimerComplete())
  )
);
```

This is a slightly contrived example, but I have done similar things in the past. One particular case involved waiting for a few seconds so that a user could read a notification before they were redirected elsewhere.

To test this we need to abandon marbles though!

```ts
it('should dispatch after a delay (fakeAsync)', fakeAsync(() => {
  actions = of(ThingActions.startThingTimer());

  let output;
  effects.timedDispatchEffect$.subscribe((action) => {
    output = action;
  });

  expect(output).toBeUndefined();

  tick(ThingsEffects.timerDuration);

  expect(output).toEqual(ThingActions.thingTimerComplete());
}));
```

Angular handily provides us with the [fakeAsync](https://angular.io/api/core/testing/fakeAsync) function which lets us control the flow of time. [delay](https://rxjs-dev.firebaseapp.com/api/operators/delay) has its concept of time based on the scheduler it uses, so in order to test this with marbles we would have to (somehow) tell it that we want to use the [TestScheduler](https://rxjs-dev.firebaseapp.com/api/testing/TestScheduler) alongside hot and cold rather than the default [async Scheduler](https://rxjs-dev.firebaseapp.com/guide/scheduler#scheduler-types). This wouldn't be a trivial thing to do as often these kinds of operators are buried deep in your effect and you really don't want to have to start injecting schedulers into your effects. It's simpler just to discard marbles entirely and test it with `fakeAsync`.

With `fakeAsync` we set up a normal subscription to the effect as we would in non-test code and then trigger it by ticking forward time with a function appropriately called `tick`. When we tick far enough, the observer will be triggered, `output` will be populated and we can check that it matches what we expect!

---

That brings us to the end of these patterns with the important point that there is always another way to test them. You don't have to use marbles at all, in fact it could be argued that they make things more complicated for these kinds of cases not less! That decision is up to you. Don't worry too much about what you decide as long as it makes sense to you. There's never any point in sticking with something you find confusing. Do what works for you.

As always, if you have any questions, corrections or comments, feel free to get in touch here or on [Twitter](https://twitter.com/jdpearce).