Skip to content

Commit 1650582

Browse files
feat(store): add support for provideMockStore outside of the TestBed (#2759)
Closes #2745
1 parent 93a4754 commit 1650582

File tree

2 files changed

+288
-10
lines changed

2 files changed

+288
-10
lines changed

modules/store/testing/spec/mock_store.spec.ts

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { TestBed, ComponentFixture } from '@angular/core/testing';
22
import { skip, take } from 'rxjs/operators';
3-
import { MockStore, provideMockStore } from '@ngrx/store/testing';
3+
import {
4+
getMockStore,
5+
MockReducerManager,
6+
MockState,
7+
MockStore,
8+
provideMockStore,
9+
} from '@ngrx/store/testing';
410
import {
511
Store,
612
createSelector,
@@ -9,9 +15,14 @@ import {
915
MemoizedSelector,
1016
createFeatureSelector,
1117
isNgrxMockEnvironment,
18+
INITIAL_STATE,
19+
ActionsSubject,
20+
INIT,
21+
StateObservable,
22+
ReducerManager,
1223
} from '@ngrx/store';
1324
import { INCREMENT } from '../../spec/fixtures/counter';
14-
import { Component } from '@angular/core';
25+
import { Component, Injector } from '@angular/core';
1526
import { Observable } from 'rxjs';
1627
import { By } from '@angular/platform-browser';
1728

@@ -22,7 +33,7 @@ interface TestAppSchema {
2233
counter4?: number;
2334
}
2435

25-
describe('Mock Store', () => {
36+
describe('Mock Store with TestBed', () => {
2637
let mockStore: MockStore<TestAppSchema>;
2738
const initialState = { counter1: 0, counter2: 1, counter4: 3 };
2839
const stringSelector = 'counter4';
@@ -281,6 +292,150 @@ describe('Mock Store', () => {
281292
});
282293
});
283294

295+
describe('Mock Store with Injector', () => {
296+
const initialState = { counter: 0 } as const;
297+
const mockSelector = { selector: 'counter', value: 10 } as const;
298+
299+
describe('Injector.create', () => {
300+
let injector: Injector;
301+
302+
beforeEach(() => {
303+
injector = Injector.create({
304+
providers: [
305+
provideMockStore({ initialState, selectors: [mockSelector] }),
306+
],
307+
});
308+
});
309+
310+
it('should set NgrxMockEnvironment to true', () => {
311+
expect(isNgrxMockEnvironment()).toBe(true);
312+
});
313+
314+
it('should provide Store', (done) => {
315+
const store: Store<typeof initialState> = injector.get(Store);
316+
317+
store.pipe(take(1)).subscribe((state) => {
318+
expect(state).toBe(initialState);
319+
done();
320+
});
321+
});
322+
323+
it('should provide MockStore', (done) => {
324+
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);
325+
326+
mockStore.pipe(take(1)).subscribe((state) => {
327+
expect(state).toBe(initialState);
328+
done();
329+
});
330+
});
331+
332+
it('should provide the same instance for Store and MockStore', () => {
333+
const store: Store<typeof initialState> = injector.get(Store);
334+
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);
335+
336+
expect(store).toBe(mockStore);
337+
});
338+
339+
it('should use a mock selector', (done) => {
340+
const mockStore: MockStore<typeof initialState> = injector.get(MockStore);
341+
342+
mockStore
343+
.select(mockSelector.selector)
344+
.pipe(take(1))
345+
.subscribe((selectedValue) => {
346+
expect(selectedValue).toBe(mockSelector.value);
347+
done();
348+
});
349+
});
350+
351+
it('should provide INITIAL_STATE', () => {
352+
const providedInitialState = injector.get(INITIAL_STATE);
353+
354+
expect(providedInitialState).toBe(initialState);
355+
});
356+
357+
it('should provide ActionsSubject', (done) => {
358+
const actionsSubject = injector.get(ActionsSubject);
359+
360+
actionsSubject.pipe(take(1)).subscribe((action) => {
361+
expect(action.type).toBe(INIT);
362+
done();
363+
});
364+
});
365+
366+
it('should provide MockState', (done) => {
367+
const mockState: MockState<typeof initialState> = injector.get(MockState);
368+
369+
mockState.pipe(take(1)).subscribe((state) => {
370+
expect(state).toEqual({});
371+
done();
372+
});
373+
});
374+
375+
it('should provide StateObservable', (done) => {
376+
const stateObservable = injector.get(StateObservable);
377+
378+
stateObservable.pipe(take(1)).subscribe((state) => {
379+
expect(state).toEqual({});
380+
done();
381+
});
382+
});
383+
384+
it('should provide the same instance for MockState and StateObservable', () => {
385+
const mockState: MockState<typeof initialState> = injector.get(MockState);
386+
const stateObservable: StateObservable = injector.get(StateObservable);
387+
388+
expect(mockState).toBe(stateObservable);
389+
});
390+
391+
it('should provide ReducerManager', () => {
392+
const reducerManager = injector.get(ReducerManager);
393+
394+
expect(reducerManager.addFeature).toEqual(expect.any(Function));
395+
expect(reducerManager.addFeatures).toEqual(expect.any(Function));
396+
});
397+
398+
it('should provide MockReducerManager', () => {
399+
const mockReducerManager = injector.get(MockReducerManager);
400+
401+
expect(mockReducerManager.addFeature).toEqual(expect.any(Function));
402+
expect(mockReducerManager.addFeatures).toEqual(expect.any(Function));
403+
});
404+
405+
it('should provide the same instance for ReducerManager and MockReducerManager', () => {
406+
const reducerManager = injector.get(ReducerManager);
407+
const mockReducerManager = injector.get(MockReducerManager);
408+
409+
expect(reducerManager).toBe(mockReducerManager);
410+
});
411+
});
412+
413+
describe('getMockStore', () => {
414+
let mockStore: MockStore<typeof initialState>;
415+
416+
beforeEach(() => {
417+
mockStore = getMockStore({ initialState, selectors: [mockSelector] });
418+
});
419+
420+
it('should create MockStore', (done) => {
421+
mockStore.pipe(take(1)).subscribe((state) => {
422+
expect(state).toBe(initialState);
423+
done();
424+
});
425+
});
426+
427+
it('should use a mock selector', (done) => {
428+
mockStore
429+
.select(mockSelector.selector)
430+
.pipe(take(1))
431+
.subscribe((selectedValue) => {
432+
expect(selectedValue).toBe(mockSelector.value);
433+
done();
434+
});
435+
});
436+
});
437+
});
438+
284439
describe('Refreshing state', () => {
285440
type TodoState = {
286441
items: { name: string; done: boolean }[];

modules/store/testing/src/testing.ts

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { Provider } from '@angular/core';
1+
import {
2+
ExistingProvider,
3+
FactoryProvider,
4+
Injector,
5+
ValueProvider,
6+
} from '@angular/core';
27
import { MockState } from './mock_state';
38
import {
49
ActionsSubject,
@@ -18,22 +23,140 @@ export interface MockStoreConfig<T> {
1823
selectors?: MockSelector[];
1924
}
2025

26+
/**
27+
* @description
28+
* Creates mock store providers.
29+
*
30+
* @param config `MockStoreConfig<T>` to provide the values for `INITIAL_STATE` and `MOCK_SELECTORS` tokens.
31+
* By default, `initialState` and `selectors` are not defined.
32+
* @returns Mock store providers that can be used with both `TestBed.configureTestingModule` and `Injector.create`.
33+
*
34+
* @usageNotes
35+
*
36+
* **With `TestBed.configureTestingModule`**
37+
*
38+
* ```typescript
39+
* describe('Books Component', () => {
40+
* let store: MockStore;
41+
*
42+
* beforeEach(() => {
43+
* TestBed.configureTestingModule({
44+
* providers: [
45+
* provideMockStore({
46+
* initialState: { books: { entities: [] } },
47+
* selectors: [
48+
* { selector: selectAllBooks, value: ['Book 1', 'Book 2'] },
49+
* { selector: selectVisibleBooks, value: ['Book 1'] },
50+
* ],
51+
* }),
52+
* ],
53+
* });
54+
*
55+
* store = TestBed.inject(MockStore);
56+
* });
57+
* });
58+
* ```
59+
*
60+
* **With `Injector.create`**
61+
*
62+
* ```typescript
63+
* describe('Counter Component', () => {
64+
* let injector: Injector;
65+
* let store: MockStore;
66+
*
67+
* beforeEach(() => {
68+
* injector = Injector.create({
69+
* providers: [
70+
* provideMockStore({ initialState: { counter: 0 } }),
71+
* ],
72+
* });
73+
* store = injector.get(MockStore);
74+
* });
75+
* });
76+
* ```
77+
*/
2178
export function provideMockStore<T = any>(
2279
config: MockStoreConfig<T> = {}
23-
): Provider[] {
80+
): (ValueProvider | ExistingProvider | FactoryProvider)[] {
2481
setNgrxMockEnvironment(true);
2582
return [
26-
ActionsSubject,
27-
MockState,
28-
MockStore,
83+
{
84+
provide: ActionsSubject,
85+
useFactory: () => new ActionsSubject(),
86+
deps: [],
87+
},
88+
{ provide: MockState, useFactory: () => new MockState<T>(), deps: [] },
89+
{
90+
provide: MockReducerManager,
91+
useFactory: () => new MockReducerManager(),
92+
deps: [],
93+
},
2994
{ provide: INITIAL_STATE, useValue: config.initialState || {} },
3095
{ provide: MOCK_SELECTORS, useValue: config.selectors },
31-
{ provide: StateObservable, useClass: MockState },
32-
{ provide: ReducerManager, useClass: MockReducerManager },
96+
{ provide: StateObservable, useExisting: MockState },
97+
{ provide: ReducerManager, useExisting: MockReducerManager },
98+
{
99+
provide: MockStore,
100+
useFactory: mockStoreFactory,
101+
deps: [
102+
MockState,
103+
ActionsSubject,
104+
ReducerManager,
105+
INITIAL_STATE,
106+
MOCK_SELECTORS,
107+
],
108+
},
33109
{ provide: Store, useExisting: MockStore },
34110
];
35111
}
36112

113+
function mockStoreFactory<T>(
114+
mockState: MockState<T>,
115+
actionsSubject: ActionsSubject,
116+
reducerManager: ReducerManager,
117+
initialState: T,
118+
mockSelectors: MockSelector[]
119+
): MockStore<T> {
120+
return new MockStore(
121+
mockState,
122+
actionsSubject,
123+
reducerManager,
124+
initialState,
125+
mockSelectors
126+
);
127+
}
128+
129+
/**
130+
* @description
131+
* Creates mock store with all necessary dependencies outside of the `TestBed`.
132+
*
133+
* @param config `MockStoreConfig<T>` to provide the values for `INITIAL_STATE` and `MOCK_SELECTORS` tokens.
134+
* By default, `initialState` and `selectors` are not defined.
135+
* @returns `MockStore<T>`
136+
*
137+
* @usageNotes
138+
*
139+
* ```typescript
140+
* describe('Books Effects', () => {
141+
* let store: MockStore;
142+
*
143+
* beforeEach(() => {
144+
* store = getMockStore({
145+
* initialState: { books: { entities: ['Book 1', 'Book 2', 'Book 3'] } },
146+
* selectors: [
147+
* { selector: selectAllBooks, value: ['Book 1', 'Book 2'] },
148+
* { selector: selectVisibleBooks, value: ['Book 1'] },
149+
* ],
150+
* });
151+
* });
152+
* });
153+
* ```
154+
*/
155+
export function getMockStore<T>(config: MockStoreConfig<T> = {}): MockStore<T> {
156+
const injector = Injector.create({ providers: provideMockStore(config) });
157+
return injector.get(MockStore);
158+
}
159+
37160
export { MockReducerManager } from './mock_reducer_manager';
38161
export { MockState } from './mock_state';
39162
export { MockStore } from './mock_store';

0 commit comments

Comments
 (0)