Skip to content

Instantly share code, notes, and snippets.

@honsq90
Created October 4, 2018 23:16
Show Gist options
  • Save honsq90/dcf5783593c94084c83d4e34d93479b6 to your computer and use it in GitHub Desktop.
Save honsq90/dcf5783593c94084c83d4e34d93479b6 to your computer and use it in GitHub Desktop.
ngRx 6 Mock Store
import * as ngrxStore from '@ngrx/store';
import { MemoizedSelector } from '@ngrx/store';
import { BehaviorSubject, pipe } from 'rxjs';
import { map } from 'rxjs/operators';
interface SelectorMap {
[key: string]: MemoizedSelector<any, any>;
}
function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
}
export class Ngrx6MockStore extends BehaviorSubject<any> {
private state: any = {};
select;
constructor(private selectors: SelectorMap) {
super({});
this.select = jest
.spyOn(ngrxStore, 'select')
.mockImplementation((selector) => {
const selectKey = getKeyByValue(this.selectors, selector);
return pipe(
map((state) => {
return state[selectKey];
})
);
});
}
dispatch = jest.fn();
mockNext(override: object): void {
this.state = {
...this.state,
...override,
};
this.next(this.state);
}
}
export function provideMockStore<T>(selectors: SelectorMap) {
return [
{
provide: ngrxStore.Store,
useValue: new Ngrx6MockStore(selectors),
},
];
}
@honsq90
Copy link
Author

honsq90 commented Oct 17, 2018

Following discussions from:
ngrx/platform#915
ngrx/platform#1027

Since the new injected Store is a BehaviorSubject and we've already tested our reducers, effects and selectors, this mock store is a very stripped down version on the proposed testing solution.

Similar to some of the examples in the linked issue, this would allow us greater control over the various permutations of selector observables to test the component. This way we don't have to worry about setting up state via dispatching actions in our unit tests, all the component cares about is the contract set up with the NgRx selectors. If the internals of the selectors and/or the reducers were to change, we wouldn't have to update our test setup scripts.

Here's how you would utilise the mock store:

import { Component } from '@angular/core';
import { select, Store, createSelector, createFeatureSelector } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

interface LoadableStore {
  data: string[];
  error: string | null;
  pending: boolean;
}

const dataFeatureSelector = createFeatureSelector('data_store');
const getData = createSelector(dataFeatureSelector, (store: LoadableStore) => store.data);
const getDataError = createSelector(dataFeatureSelector, (store: LoadableStore) => store.error);
const getDataPending = createSelector(dataFeatureSelector, (store: LoadableStore) => store.pending);


@Component({
  templateUrl: './test.component.html',
})
export class TestComponent {
  data$: Observable<string[]>;
  dataError$: Observable<string|null>;
  dataPending$: Observable<boolean>;

  constructor(private store: Store<any>) {
    this.data$ = this.store.pipe(select(getData));
    this.dataError$ = this.store.pipe(select(getDataError));
    this.dataPending$ = this.store.pipe(select(getDataPending));
  }
}

Your corresponding test would look like this:

...
import { provideMockStore, Store } from './ngrx6-mock-store';
import * as dataSelectors from 'path/to/selectors';

describe('TestComponemt', () => {
  let component: TestComponemt;
  let fixture: ComponentFixture<TestComponemt>;
  let store$: Ngrx6MockStore;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [TestComponemt],
      providers: [provideMockStore(dataSelectors)],
      schemas: [ NO_ERRORS_SCHEMA ],
    })
      .compileComponents();
  }));

  beforeEach(() => {
    store$ = TestBed.get(Store);
    fixture = TestBed.createComponent(TestComponemt);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should call the right selectors', () => {
    expect(store$.select).toHaveBeenCalledWith(getData);
    expect(store$.select).toHaveBeenCalledWith(getDataError);
    expect(store$.select).toHaveBeenCalledWith(getDataPending);
  });

  it('should not render if pending', () => {
    store$.mockNext({
      getDataPending: true,
    });
    fixture.detectChanges();
    expect(fixture.debugElement.query('.pending')).toBeTruthy();
    expect(fixture.debugElement.query('.error')).toBeFalsy();
    expect(fixture.debugElement.query('.data')).toBeFalsy();
  });

  it('should render if no errors', () => {
    store$.mockNext({
      getData: ['data1', 'data2'],
      getDataPending: false,
      getDataError: null,
    });
    fixture.detectChanges();
    expect(fixture.debugElement.query('.pending')).toBeFalsy();
    expect(fixture.debugElement.query('.error')).toBeFalsy();
    expect(fixture.debugElement.query('.data')).toBeTruthy();
  });

});

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