Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
bobvandenberge opened this issue Sep 20, 2017 · 19 comments

Comments

@bobvandenberge
Copy link

bobvandenberge commented Sep 20, 2017

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) {
}

@phillipzada
Copy link
Contributor

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

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

@nathanmarks
Copy link

@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.

@bobvandenberge
Copy link
Author

@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
    });

@phillipzada
Copy link
Contributor

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));
}

@michaelsaprykin
Copy link

michaelsaprykin commented Sep 22, 2017

@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));

@bobvandenberge
Copy link
Author

bobvandenberge commented Sep 22, 2017

@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();
  });

@jakefreeberg
Copy link

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

@kazinov
Copy link

kazinov commented May 17, 2018

@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);
  });

@alan-agius4
Copy link
Contributor

alan-agius4 commented May 31, 2018

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
	});
});

@kevinbeal
Copy link

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.

@rkorrelboom
Copy link

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);
    });
  });
});

@fotopixel
Copy link

fotopixel commented Sep 13, 2018

@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

@bobvandenberge
Copy link
Author

@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.

@jtcrowson
Copy link
Contributor

@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>).

@EugeneSnihovsky
Copy link

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)

@timdeschryver
Copy link
Member

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

@brunano21
Copy link

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?

@swapnil0545
Copy link

swapnil0545 commented Mar 7, 2019

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.

@stianmorsund
Copy link

stianmorsund commented Apr 3, 2019

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:

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

In effect.spec:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests