Platform: Documentation request: how to setup State while testing Effects that use withLatestFrom

Created on 20 Sep 2017  路  19Comments  路  Source: ngrx/platform

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ ] Feature request
[x] Documentation issue or request

We recently started using ngrx. Current we use marbles to test our Effects which works great in almost all scenario's. We have come across our first scenario where we have to test an Effect that uses a value from the store. In the code we use withLatestFrom for this. The problem is that we cannot figure out how to get this working in the test. How can we setup test data in the store? Can we update the documentation with an example of this? I would be surprised if I am the only one facing this issue :).

Code:
Snippit from Effects.ts

@Effect()
closeModal$ = this.actions$
.ofType(CLOSE_MODAL)
.withLatestFrom(this.store)
.switchMap(([action, state], _) => {
// state is always an empty Object({}) here while we want it to be a mock/fake state
return Observable.empty();
});

constructor(private actions$: Actions, private store: Store) {
}

Docs

Most helpful comment

@phillipzada @MikeSaprykin Thanks for the suggestions! We managed to solve the issue using an adapted version of the solution provided by Mike.

We have a custom FakeStore that allows us to easily setup mock data for selectors. Next to that I adjusted our withLatestFrom to use a selector instead of the complete store. This allows us to mock the state using our own mock store.

2 questions/topics remain though (not sure if they are part of this issue):

  1. Should we document these solutions somewhere? For users new to ngrx (like us) these kind of solutions are not obvious and take quite some time to research.
  2. Should @ngrx/store provide an out-of-the-box solution for setting up mock state?

For further reference, here is the complete code we use.

FakeStore.ts:

@Injectable()
export class FakeStore extends Observable<any> {

  reducers = new Map<any, BehaviorSubject<any>>();

  constructor() {
    super();
  }

  /**
   * simple solution to support selecting/subscribing to this mockstore as usual.
   * @param reducer The reducer
   * @returns {undefined|BehaviorSubject<any>}
   */
  public select(reducer): BehaviorSubject<any> {
    if (!this.reducers.has(reducer)) {
      this.reducers.set(reducer, new BehaviorSubject({}));
    }
    return this.reducers.get(reducer);
  }

  /**
   * used to set a fake state
   * @param reducer name of your reducer
   * @param data the mockstate you want to have
   */
  mockState(reducer, data) {
    this.select(reducer).next(data);
  }
}

effects.ts:

@Effect()
  closeModal$ = this.actions$
    .ofType(CLOSE_MODAL)
    .withLatestFrom(**this.store.select(selectLayoutModal)**)
    .switchMap(([action, modal], _) => {
      if (modal) {
        modal.close();
        return of(new ModalClosed());
      } else {
        return Observable.empty();
      }
    });

effects.spec.ts:

TestBed.configureTestingModule({
      imports: [
        StoreTestingModule <-- Overwrites the Store and loads our FakeStore instead
      ],
      providers: [
        LayoutEffects,
        provideMockActions(() => actions)
      ]
    });

// Lots of code left out for clarity

it('on CLOSE_MODAL should close Modal and dispatch ModalClosed Action', () => {
    const mdDialogRef = {close: jasmine.createSpy('close')};
    store.mockState(selectLayoutModal, mdDialogRef); // store here is our FakeStore

    actions = hot('a', {a: new CloseModal()});
    const expected = cold('a', {a: new ModalClosed()});

    expect(effects.closeModal$).toBeObservable(expected);
    expect(mdDialogRef.close).toHaveBeenCalled();
  });

All 19 comments

Have you setup the TestingModule with a store and a state? i.e.

TestBed.configureTestingModule({
            imports: [
                StoreModule.forRoot({

@phillipzada If you need to test with different state values -- reconfiguring the test bed multiple times feels like a lot of work.

In my own tests... I've resorted to dispatching actions that will updated the necessary state values... but it feels like a bad workaround. It would be ideal to have a way to set the state values without reconfiguring the test bed and without having to dispatch actions from your app.

@phillipzada Yes, we have setup the TestModule. The main problem is that we can only setup one state here, as @nathanmarks notes. For now, we use the work around that Nathan suggests but it is a very brittle solution. It would be nice if there was a officially supported way to setup the test data in the Store.

It would be nice if, for tests, we would be able to do something like:

store.setLatestState({
      // Some valid state for the store
    });

Hey guys,

What we've done is create meta reducer specifically for tests that updates the state for each test. Remembering you have a pretty good idea where your state should be at when a particular action is fired and picked up by an effect.

export function mockMetaReducer(reducer: ActionReducer<any>): ActionReducer<any, any> {
    return function (state: any, action: any): any {

        switch (action.type) {
            case mock.ActionTypes.ASSIGN_STATE:
                return reducer({...state, ...action.payload}, action);
        }

        return reducer(state, action);
    };
}

then during the test its injected into the beforeEach TestModule as so:

import * as storeHelpers from 'app/shared/spec-helpers/store.helpers.spec';
...
beforeEach(...
TestBed.configureTestingModule({
            imports: [
                StoreModule.forRoot(
                    { ...fromRoot.reducers }, 
                    { metaReducers: [storeHelpers.mockMetaReducer] }
            )]
)

Note: if you append the file that contains the metaReducer with .spec.ts it will only get compiled in test runs.

Then in your tests you have the following function that can be called from your tests

function updateState(value: any) {
   store.dispatch(new mock.MockAssignStateAction(value));
}

@bobvandenberge - In testing effects, If you are getting the state via store.select (which is recommended) you can inject only the provider in TestBed. You don't need to inject the full StoreModule.
So you can inject mocked provider in TestBed like that:

let store: Store<any>

class MockStore {
  select(){}
}

TestBed.configureTestingModule({
 providers: [
   {
      provide: Store,
      useClass: MockStore
   }
]
});
store = TestBed.get(Store);

And in test suite you can use Spy to give you any slice of store that you want:

spyOn(store, 'select').and.returnValue(of(initialState));

@phillipzada @MikeSaprykin Thanks for the suggestions! We managed to solve the issue using an adapted version of the solution provided by Mike.

We have a custom FakeStore that allows us to easily setup mock data for selectors. Next to that I adjusted our withLatestFrom to use a selector instead of the complete store. This allows us to mock the state using our own mock store.

2 questions/topics remain though (not sure if they are part of this issue):

  1. Should we document these solutions somewhere? For users new to ngrx (like us) these kind of solutions are not obvious and take quite some time to research.
  2. Should @ngrx/store provide an out-of-the-box solution for setting up mock state?

For further reference, here is the complete code we use.

FakeStore.ts:

@Injectable()
export class FakeStore extends Observable<any> {

  reducers = new Map<any, BehaviorSubject<any>>();

  constructor() {
    super();
  }

  /**
   * simple solution to support selecting/subscribing to this mockstore as usual.
   * @param reducer The reducer
   * @returns {undefined|BehaviorSubject<any>}
   */
  public select(reducer): BehaviorSubject<any> {
    if (!this.reducers.has(reducer)) {
      this.reducers.set(reducer, new BehaviorSubject({}));
    }
    return this.reducers.get(reducer);
  }

  /**
   * used to set a fake state
   * @param reducer name of your reducer
   * @param data the mockstate you want to have
   */
  mockState(reducer, data) {
    this.select(reducer).next(data);
  }
}

effects.ts:

@Effect()
  closeModal$ = this.actions$
    .ofType(CLOSE_MODAL)
    .withLatestFrom(**this.store.select(selectLayoutModal)**)
    .switchMap(([action, modal], _) => {
      if (modal) {
        modal.close();
        return of(new ModalClosed());
      } else {
        return Observable.empty();
      }
    });

effects.spec.ts:

TestBed.configureTestingModule({
      imports: [
        StoreTestingModule <-- Overwrites the Store and loads our FakeStore instead
      ],
      providers: [
        LayoutEffects,
        provideMockActions(() => actions)
      ]
    });

// Lots of code left out for clarity

it('on CLOSE_MODAL should close Modal and dispatch ModalClosed Action', () => {
    const mdDialogRef = {close: jasmine.createSpy('close')};
    store.mockState(selectLayoutModal, mdDialogRef); // store here is our FakeStore

    actions = hot('a', {a: new CloseModal()});
    const expected = cold('a', {a: new ModalClosed()});

    expect(effects.closeModal$).toBeObservable(expected);
    expect(mdDialogRef.close).toHaveBeenCalled();
  });

Thanks for this solution. I do think the "2 questions/topics" are worth more discussion.

@bobvandenberge solution is very nice. But after I adopted it in my project I realised that it doesn't suitable for more complex cases. For example you have something like this in your effect class:

  @Effect()
  effect$ = this.store.select(Selectors.getValue)
    .skip(1)
    .filter(value => !value)
    .switchMap(token => of(
        new Action();
      ));

In general sometimes you can depend on store changes over time in your effects. One solution could be to call the original actions which will cause the corresponding store changes. It's a legit solution but it makes your effect test more dependant on your application structure, less isolated.
So I changed the @bobvandenberge FakeStore class so it reacts on the original actions marble.

export interface FakeState {
  selector: Function;
  state: any;
}

// action which causes the fake action change
export const SET_FAKE_STATE = '[FakeStore] Set fake state';
export class SetFakeState implements Action {
  readonly type = SET_FAKE_STATE;
  constructor(public payload: FakeState) {
  }
}

// provides a way to mock all the selectors results through actions
@Injectable()
export class FakeStore extends Observable<any> {

  private selectors = new Map<any, Subject<any>>();
  private setFakeState$ = this.actions$
    .ofType(SET_FAKE_STATE)
    .pipe(
      tap((action: SetFakeState) => {
        if (!action.payload) {
          return;
        }
        this.select(action.payload.selector).next(action.payload.state);
      })
    );

  constructor(private actions$: Actions) {
    super();
  }

  subscribeStateChanges() {
    this.setFakeState$.subscribe();
  }

  select(selector): Subject<any> {
    if (!this.selectors.has(selector)) {
      this.selectors.set(selector, new Subject<any>());
    }
    return this.selectors.get(selector);
  }

  dispatch(action: any) {}

}

in your spec it looks somehow like this:

  it('should call Action if the value became falsy', () => {
    actions$ = hot('ab', {
      a: new SetFakeState({
        selector: Selectors.getValue,
        state: 'value'
      }),
      b: new SetFakeState({
        selector: Selectors.getValue,
        state: null
      })
    });
    const expected = cold('-b', {
      b: new Action()
    });
    store.subscribeStateChanges();
    expect(effects.effect$).toBeObservable(expected);
  });

I ended up doing something like

export class MockStore<T = any> extends Store<T> {
    source = new BehaviorSubject<any>({});
    overrideState(state: any) {
        this.source.next(state);
    }

}

.spec.ts

TestBed.configureTestingModule({
    providers: [
        { provide: Store, useClass: MockStore },
    ]
});

beforeEach(() => {
    store.overrideState({
        route: MOCK_ROUTE_DATA_WITH_OVERRIDE
    });
});

I was getting errors indicating that there was no initial state in my library tests.

I found that in my library module I had to use the static EffectsModule.forFeature() and StoreModule.forFeature(), but in my tests, I had to use the .forRoot() method on both.

Another option is to TestBed.get(<effects>) in your test or describe instead of in the beforeEach at root level like so:

describe('SomeEffects', () => {
  let actions$: Observable<any>;
  let effects: SomeEffects;
  let store: SpyObj<Store<SomeState>>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        SomeEffects,
        provideMockActions(() => actions$),
        {
          provide: Store,
          useValue: jasmine.createSpyObj('store', [ 'select' ])
        }
      ]
    });

    // IMPORTANT: Don't get the effects here to delay instantiation of the class until needed! 
    store = TestBed.get(Store);
  });

  it('should be created', () => {
    effects = TestBed.get(SomeEffects); // Instance is created here
    expect(effects).toBeTruthy();
  });

  describe('someSimpleEffect$', () => {
    beforeEach(() => {
      effects = TestBed.get(SomeEffects); // Instantiate in before each of nested describe for tests that don't need constructor mocks
    });
  });

  describe('someWithLatestEffect$', () => {
    it('should instantiate SomeEffects After initializing the mock', () => {
      const action = new SomeAction();
      const completion = new SomeActionSuccess();

      store.select.and.returnValue(cold('r', { r: true )); // Initialize mock here

      effects = TestBed.get(SomeEffect); // Instantiate SomeEffects here so we can use the mock

      actions$ = hot('a', { a: action });
      const expected = cold('b', { b: completion });

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

@bobvandenberge how can i use this example if i use

withLatestFrom(this.store.pipe(select(someValue)))

we refactor it from this withLatestFrom(this.store.select(someValue))) because it is deprecated

@FooPix we haven't upgraded yet so I don't have an answer for that. Once we upgrade and figure it out I will let you know.

@rkorrelboom's approach can be used with whatever mock library you're using (ex: jest). The idea is to overwrite your store mock before you call TestBed.get(<effects>).

Probably it make sense to add this or other acceptable solution to compiled module and add documentation for it. I spent 2 hours to write simple unit test for effect with withLatestFrom(this._store)

@EugeneSnihovsky NgRx 7.0 introduced a MockStore implementation but isn't documented yet. See #1027.

Hey lads, I'm having some difficulties in unit testing an effect which uses withLatestFrom(this.store.pipe(select(getFoo))). Does anyone have a hint about?

Yes, this kind of documentation is indeed needed that too along with new Mockstore implementation. I don't think this should be closed unless, its new example is added.

I've found the best way to do this is to use a Facade instead of injecting the Store into the effects. That way, you can easily mock only the things you want without having to deal with the whole Store.

In effect:

```(javascript)
....
withLatestFrom(this.fooFacade.bar$)

In effect.spec:
```(javascript)
class MockFooFacade {
  get bar$() {
    return of(MOCKED_VALUE)
  }
}
// Then provide it in TestBed:
...
{ provide: FooFacade, useClass: MockFooFacade },

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hccampos picture hccampos  路  3Comments

sandangel picture sandangel  路  3Comments

oxiumio picture oxiumio  路  3Comments

dmytro-gokun picture dmytro-gokun  路  3Comments

brandonroberts picture brandonroberts  路  3Comments