Skip to content

Commit

Permalink
feat(store): add support for provideMockStore outside of the TestBed (#…
Browse files Browse the repository at this point in the history
…2759)

Closes #2745
  • Loading branch information
markostanimirovic authored Nov 15, 2020
1 parent 93a4754 commit 1650582
Show file tree
Hide file tree
Showing 2 changed files with 288 additions and 10 deletions.
161 changes: 158 additions & 3 deletions modules/store/testing/spec/mock_store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { skip, take } from 'rxjs/operators';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import {
getMockStore,
MockReducerManager,
MockState,
MockStore,
provideMockStore,
} from '@ngrx/store/testing';
import {
Store,
createSelector,
Expand All @@ -9,9 +15,14 @@ import {
MemoizedSelector,
createFeatureSelector,
isNgrxMockEnvironment,
INITIAL_STATE,
ActionsSubject,
INIT,
StateObservable,
ReducerManager,
} from '@ngrx/store';
import { INCREMENT } from '../../spec/fixtures/counter';
import { Component } from '@angular/core';
import { Component, Injector } from '@angular/core';
import { Observable } from 'rxjs';
import { By } from '@angular/platform-browser';

Expand All @@ -22,7 +33,7 @@ interface TestAppSchema {
counter4?: number;
}

describe('Mock Store', () => {
describe('Mock Store with TestBed', () => {
let mockStore: MockStore<TestAppSchema>;
const initialState = { counter1: 0, counter2: 1, counter4: 3 };
const stringSelector = 'counter4';
Expand Down Expand Up @@ -281,6 +292,150 @@ describe('Mock Store', () => {
});
});

describe('Mock Store with Injector', () => {
const initialState = { counter: 0 } as const;
const mockSelector = { selector: 'counter', value: 10 } as const;

describe('Injector.create', () => {
let injector: Injector;

beforeEach(() => {
injector = Injector.create({
providers: [
provideMockStore({ initialState, selectors: [mockSelector] }),
],
});
});

it('should set NgrxMockEnvironment to true', () => {
expect(isNgrxMockEnvironment()).toBe(true);
});

it('should provide Store', (done) => {
const store: Store<typeof initialState> = injector.get(Store);

store.pipe(take(1)).subscribe((state) => {
expect(state).toBe(initialState);
done();
});
});

it('should provide MockStore', (done) => {
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);

mockStore.pipe(take(1)).subscribe((state) => {
expect(state).toBe(initialState);
done();
});
});

it('should provide the same instance for Store and MockStore', () => {
const store: Store<typeof initialState> = injector.get(Store);
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);

expect(store).toBe(mockStore);
});

it('should use a mock selector', (done) => {
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);

mockStore
.select(mockSelector.selector)
.pipe(take(1))
.subscribe((selectedValue) => {
expect(selectedValue).toBe(mockSelector.value);
done();
});
});

it('should provide INITIAL_STATE', () => {
const providedInitialState = injector.get(INITIAL_STATE);

expect(providedInitialState).toBe(initialState);
});

it('should provide ActionsSubject', (done) => {
const actionsSubject = injector.get(ActionsSubject);

actionsSubject.pipe(take(1)).subscribe((action) => {
expect(action.type).toBe(INIT);
done();
});
});

it('should provide MockState', (done) => {
const mockState: MockState<typeof initialState> = injector.get(MockState);

mockState.pipe(take(1)).subscribe((state) => {
expect(state).toEqual({});
done();
});
});

it('should provide StateObservable', (done) => {
const stateObservable = injector.get(StateObservable);

stateObservable.pipe(take(1)).subscribe((state) => {
expect(state).toEqual({});
done();
});
});

it('should provide the same instance for MockState and StateObservable', () => {
const mockState: MockState<typeof initialState> = injector.get(MockState);
const stateObservable: StateObservable = injector.get(StateObservable);

expect(mockState).toBe(stateObservable);
});

it('should provide ReducerManager', () => {
const reducerManager = injector.get(ReducerManager);

expect(reducerManager.addFeature).toEqual(expect.any(Function));
expect(reducerManager.addFeatures).toEqual(expect.any(Function));
});

it('should provide MockReducerManager', () => {
const mockReducerManager = injector.get(MockReducerManager);

expect(mockReducerManager.addFeature).toEqual(expect.any(Function));
expect(mockReducerManager.addFeatures).toEqual(expect.any(Function));
});

it('should provide the same instance for ReducerManager and MockReducerManager', () => {
const reducerManager = injector.get(ReducerManager);
const mockReducerManager = injector.get(MockReducerManager);

expect(reducerManager).toBe(mockReducerManager);
});
});

describe('getMockStore', () => {
let mockStore: MockStore<typeof initialState>;

beforeEach(() => {
mockStore = getMockStore({ initialState, selectors: [mockSelector] });
});

it('should create MockStore', (done) => {
mockStore.pipe(take(1)).subscribe((state) => {
expect(state).toBe(initialState);
done();
});
});

it('should use a mock selector', (done) => {
mockStore
.select(mockSelector.selector)
.pipe(take(1))
.subscribe((selectedValue) => {
expect(selectedValue).toBe(mockSelector.value);
done();
});
});
});
});

describe('Refreshing state', () => {
type TodoState = {
items: { name: string; done: boolean }[];
Expand Down
137 changes: 130 additions & 7 deletions modules/store/testing/src/testing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Provider } from '@angular/core';
import {
ExistingProvider,
FactoryProvider,
Injector,
ValueProvider,
} from '@angular/core';
import { MockState } from './mock_state';
import {
ActionsSubject,
Expand All @@ -18,22 +23,140 @@ export interface MockStoreConfig<T> {
selectors?: MockSelector[];
}

/**
* @description
* Creates mock store providers.
*
* @param config `MockStoreConfig<T>` to provide the values for `INITIAL_STATE` and `MOCK_SELECTORS` tokens.
* By default, `initialState` and `selectors` are not defined.
* @returns Mock store providers that can be used with both `TestBed.configureTestingModule` and `Injector.create`.
*
* @usageNotes
*
* **With `TestBed.configureTestingModule`**
*
* ```typescript
* describe('Books Component', () => {
* let store: MockStore;
*
* beforeEach(() => {
* TestBed.configureTestingModule({
* providers: [
* provideMockStore({
* initialState: { books: { entities: [] } },
* selectors: [
* { selector: selectAllBooks, value: ['Book 1', 'Book 2'] },
* { selector: selectVisibleBooks, value: ['Book 1'] },
* ],
* }),
* ],
* });
*
* store = TestBed.inject(MockStore);
* });
* });
* ```
*
* **With `Injector.create`**
*
* ```typescript
* describe('Counter Component', () => {
* let injector: Injector;
* let store: MockStore;
*
* beforeEach(() => {
* injector = Injector.create({
* providers: [
* provideMockStore({ initialState: { counter: 0 } }),
* ],
* });
* store = injector.get(MockStore);
* });
* });
* ```
*/
export function provideMockStore<T = any>(
config: MockStoreConfig<T> = {}
): Provider[] {
): (ValueProvider | ExistingProvider | FactoryProvider)[] {
setNgrxMockEnvironment(true);
return [
ActionsSubject,
MockState,
MockStore,
{
provide: ActionsSubject,
useFactory: () => new ActionsSubject(),
deps: [],
},
{ provide: MockState, useFactory: () => new MockState<T>(), deps: [] },
{
provide: MockReducerManager,
useFactory: () => new MockReducerManager(),
deps: [],
},
{ provide: INITIAL_STATE, useValue: config.initialState || {} },
{ provide: MOCK_SELECTORS, useValue: config.selectors },
{ provide: StateObservable, useClass: MockState },
{ provide: ReducerManager, useClass: MockReducerManager },
{ provide: StateObservable, useExisting: MockState },
{ provide: ReducerManager, useExisting: MockReducerManager },
{
provide: MockStore,
useFactory: mockStoreFactory,
deps: [
MockState,
ActionsSubject,
ReducerManager,
INITIAL_STATE,
MOCK_SELECTORS,
],
},
{ provide: Store, useExisting: MockStore },
];
}

function mockStoreFactory<T>(
mockState: MockState<T>,
actionsSubject: ActionsSubject,
reducerManager: ReducerManager,
initialState: T,
mockSelectors: MockSelector[]
): MockStore<T> {
return new MockStore(
mockState,
actionsSubject,
reducerManager,
initialState,
mockSelectors
);
}

/**
* @description
* Creates mock store with all necessary dependencies outside of the `TestBed`.
*
* @param config `MockStoreConfig<T>` to provide the values for `INITIAL_STATE` and `MOCK_SELECTORS` tokens.
* By default, `initialState` and `selectors` are not defined.
* @returns `MockStore<T>`
*
* @usageNotes
*
* ```typescript
* describe('Books Effects', () => {
* let store: MockStore;
*
* beforeEach(() => {
* store = getMockStore({
* initialState: { books: { entities: ['Book 1', 'Book 2', 'Book 3'] } },
* selectors: [
* { selector: selectAllBooks, value: ['Book 1', 'Book 2'] },
* { selector: selectVisibleBooks, value: ['Book 1'] },
* ],
* });
* });
* });
* ```
*/
export function getMockStore<T>(config: MockStoreConfig<T> = {}): MockStore<T> {
const injector = Injector.create({ providers: provideMockStore(config) });
return injector.get(MockStore);
}

export { MockReducerManager } from './mock_reducer_manager';
export { MockState } from './mock_state';
export { MockStore } from './mock_store';
Expand Down

0 comments on commit 1650582

Please sign in to comment.