how to mock ngrx selector in a component

烈酒焚心 提交于 2019-12-05 00:09:58

I ran into the same challenge and solved it once and for all by wrapping my selectors in services, so my components just used the service to get their data rather than directly going through the store. I found this cleaned up my code, made my tests implementation-agnostic, and made mocking much easier:

mockUserService = {
  get users$() { return of(mockUsers); },
  get otherUserRelatedData$() { return of(otherMockData); }
}

TestBed.configureTestingModule({
  providers: [{ provide: UserService, useValue: mockUserService }]
});

Before I did that however, I had to solve the issue in your question.

The solution for you will depend on where you are saving the data. If you are saving it in the constructor like:

constructor(private store: Store) {
  this.users$ = store.select(getUsers);
}

Then you will need to recreate the test component every time you want to change the value returned by the store. To do that, make a function along these lines:

const createComponent = (): MyComponent => {
  fixture = TestBed.createComponent(MyComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
  return component;
};

And then call that after you change the value of what your store spy returns:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    const cmp = createComponent();
    // proceed with assertions
  });
});

Alternatively, if you are setting the value in ngOnInit:

constructor(private store: Store) {}
ngOnInit() {
  this.users$ = this.store.select(getUsers);
}

Things are a bit easier, as you can create the component once and just recall ngOnInit every time you want to change the return value from the store:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    component.ngOnInit();
    // proceed with assertions
  });
});

I also ran into this problem and using services to wrap the selectors is no option for me, too. Especially not only for testing purposes and because I use the store to replace services.

Therefore I came up with the following (also not perfect) solution:

I use a different 'Store' for each component and each different aspect. In your example I would define the following Stores&States:

export class UserStore extends Store<UserState> {}

export class LoadingStatusStore extends Store<LoadingStatusState> {}

And inject them in the User-Component:

constructor( private userStore: UserStore, private LoadingStatusStore: 
LoadingStatusStore ) {}

Mock them inside the User-Component-Test-Class:

TestBed.configureTestingModule({
  imports: [...],
  declarations: [...],
  providers: [
    { provide: UserStore, useClass: MockStore },
    { provide: LoadingStatusStore, useClass: MockStore }
  ]
}).compileComponents();

Inject them into the beforeEach() or it() test method:

beforeEach(
  inject(
    [UserStore, LoadingStatusStore],
      (
        userStore: MockStore<UserState>,
        loadingStatusStore: MockStore<LoadingStatusState>
      ) => {...}

Then you can use them to spy on the different pipe methods:

const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
  .and.returnValue(of(false));

The drawback of this method is that you still can't test more than one part of a state of a store in one test-method. But for now this works as a workaround for me.

I created a helper like that:

class MockStore {
        constructor(public selectors: any[]) {
        }

        select(calledSelector) {
          const filteredSelectors = this.selectors.filter(s => s.selector === calledSelector);
          if (filteredSelectors.length < 1) {
            throw new Error('Some selector has not been mocked');
          }
          return cold('a', {a: filteredSelectors[0].value});
        }
 }

And now my tests look like this:

  const mock = new MockStore([
    {
      selector: selectEditMode,
      value: initialState.editMode
    },
    {
      selector: selectLoading,
      value: initialState.isLoading
    }
  ]);

  it('should be initialized', function () {
    const store = jasmine.createSpyObj('store', ['dispatch', 'select']);
    store.select.and.callFake(selector => mock.select(selector));

    const comp = new MyComponent(store);

    comp.ngOnInit();

    expect(comp.editMode$).toBeObservable(cold('a', {a: false}));
    expect(comp.isLoading$).toBeObservable(cold('a', {a: false}));
  });

You could use something like that:

spyOn(store, 'select').and.callFake(selectFake);

function pipeFake(op1: OperatorFunction<UsersState, any>): Observable<any> {
  if (op1.toString() === fromStore.getLoading.toString()) {
    return of(true);
  }

  if (op1.toString() === fromStore.getUsers.toString()) {
    return of(fakeUsers);
  }

  return of({});
}

Moving your selectors into a service will not eliminate the need to mock selectors, if you are going to test selectors themselves. ngrx now has its own way of mocking and it is described here: https://ngrx.io/guide/store/testing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!