From 16505829f314a8d82ce1d5afdf7bb9885493b4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Stanimirovi=C4=87?= Date: Sun, 15 Nov 2020 18:28:41 +0100 Subject: [PATCH] feat(store): add support for provideMockStore outside of the TestBed (#2759) Closes #2745 --- modules/store/testing/spec/mock_store.spec.ts | 161 +++++++++++++++++- modules/store/testing/src/testing.ts | 137 ++++++++++++++- 2 files changed, 288 insertions(+), 10 deletions(-) diff --git a/modules/store/testing/spec/mock_store.spec.ts b/modules/store/testing/spec/mock_store.spec.ts index 598cb563ae..f06f32b76c 100644 --- a/modules/store/testing/spec/mock_store.spec.ts +++ b/modules/store/testing/spec/mock_store.spec.ts @@ -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, @@ -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'; @@ -22,7 +33,7 @@ interface TestAppSchema { counter4?: number; } -describe('Mock Store', () => { +describe('Mock Store with TestBed', () => { let mockStore: MockStore; const initialState = { counter1: 0, counter2: 1, counter4: 3 }; const stringSelector = 'counter4'; @@ -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 = injector.get(Store); + + store.pipe(take(1)).subscribe((state) => { + expect(state).toBe(initialState); + done(); + }); + }); + + it('should provide MockStore', (done) => { + const mockStore: MockStore = 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 = injector.get(Store); + const mockStore: MockStore = injector.get(MockStore); + + expect(store).toBe(mockStore); + }); + + it('should use a mock selector', (done) => { + const mockStore: MockStore = 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 = 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 = 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; + + 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 }[]; diff --git a/modules/store/testing/src/testing.ts b/modules/store/testing/src/testing.ts index 3028e6ec49..7b8a06ba2f 100644 --- a/modules/store/testing/src/testing.ts +++ b/modules/store/testing/src/testing.ts @@ -1,4 +1,9 @@ -import { Provider } from '@angular/core'; +import { + ExistingProvider, + FactoryProvider, + Injector, + ValueProvider, +} from '@angular/core'; import { MockState } from './mock_state'; import { ActionsSubject, @@ -18,22 +23,140 @@ export interface MockStoreConfig { selectors?: MockSelector[]; } +/** + * @description + * Creates mock store providers. + * + * @param config `MockStoreConfig` 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( config: MockStoreConfig = {} -): Provider[] { +): (ValueProvider | ExistingProvider | FactoryProvider)[] { setNgrxMockEnvironment(true); return [ - ActionsSubject, - MockState, - MockStore, + { + provide: ActionsSubject, + useFactory: () => new ActionsSubject(), + deps: [], + }, + { provide: MockState, useFactory: () => new MockState(), 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( + mockState: MockState, + actionsSubject: ActionsSubject, + reducerManager: ReducerManager, + initialState: T, + mockSelectors: MockSelector[] +): MockStore { + return new MockStore( + mockState, + actionsSubject, + reducerManager, + initialState, + mockSelectors + ); +} + +/** + * @description + * Creates mock store with all necessary dependencies outside of the `TestBed`. + * + * @param config `MockStoreConfig` to provide the values for `INITIAL_STATE` and `MOCK_SELECTORS` tokens. + * By default, `initialState` and `selectors` are not defined. + * @returns `MockStore` + * + * @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(config: MockStoreConfig = {}): MockStore { + 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';