Skip to content

Commit 2a9b067

Browse files
feat(store): add API to mock selectors (#1688)
Closes #1504
1 parent 3b9b890 commit 2a9b067

17 files changed

+251
-106
lines changed

modules/store/spec/selector.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ describe('Selectors', () => {
4747
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
4848
});
4949

50+
it('should allow an override of the selector return', () => {
51+
const projectFn = jasmine.createSpy('projectionFn').and.returnValue(2);
52+
53+
const selector = createSelector(incrementOne, incrementTwo, projectFn);
54+
55+
expect(selector.projector()).toBe(2);
56+
57+
selector.setResult(5);
58+
59+
const result2 = selector({});
60+
61+
expect(result2).toBe(5);
62+
});
63+
5064
it('should be possible to test a projector fn independent from the selectors it is composed of', () => {
5165
const projectFn = jasmine.createSpy('projectionFn');
5266
const selector = createSelector(incrementOne, incrementTwo, projectFn);

modules/store/spec/store.spec.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Spy = jasmine.Spy;
2626
import any = jasmine.any;
2727
import { skip, take } from 'rxjs/operators';
2828
import { MockStore, provideMockStore } from '../testing';
29+
import { createSelector } from '../src/selector';
2930

3031
interface TestAppSchema {
3132
counter1: number;
@@ -448,10 +449,9 @@ describe('ngRx Store', () => {
448449

449450
describe('Mock Store', () => {
450451
let mockStore: MockStore<TestAppSchema>;
452+
const initialState = { counter1: 0, counter2: 1 };
451453

452454
beforeEach(() => {
453-
const initialState = { counter1: 0, counter2: 1 };
454-
455455
TestBed.configureTestingModule({
456456
providers: [provideMockStore({ initialState })],
457457
});
@@ -482,6 +482,106 @@ describe('ngRx Store', () => {
482482
.subscribe(scannedAction => expect(scannedAction).toEqual(action));
483483
mockStore.dispatch(action);
484484
});
485+
486+
it('should allow mocking of store.select with string selector', () => {
487+
const mockValue = 5;
488+
489+
mockStore.overrideSelector('counter1', mockValue);
490+
491+
mockStore
492+
.select('counter1')
493+
.subscribe(result => expect(result).toBe(mockValue));
494+
});
495+
496+
it('should allow mocking of store.select with a memoized selector', () => {
497+
const mockValue = 5;
498+
const selector = createSelector(
499+
() => initialState,
500+
state => state.counter1
501+
);
502+
503+
mockStore.overrideSelector(selector, mockValue);
504+
505+
mockStore
506+
.select(selector)
507+
.subscribe(result => expect(result).toBe(mockValue));
508+
});
509+
510+
it('should allow mocking of store.pipe(select()) with a memoized selector', () => {
511+
const mockValue = 5;
512+
const selector = createSelector(
513+
() => initialState,
514+
state => state.counter2
515+
);
516+
517+
mockStore.overrideSelector(selector, mockValue);
518+
519+
mockStore
520+
.pipe(select(selector))
521+
.subscribe(result => expect(result).toBe(mockValue));
522+
});
523+
524+
it('should pass through unmocked selectors', () => {
525+
const mockValue = 5;
526+
const selector = createSelector(
527+
() => initialState,
528+
state => state.counter1
529+
);
530+
const selector2 = createSelector(
531+
() => initialState,
532+
state => state.counter2
533+
);
534+
const selector3 = createSelector(
535+
selector,
536+
selector2,
537+
(sel1, sel2) => sel1 + sel2
538+
);
539+
540+
mockStore.overrideSelector(selector, mockValue);
541+
542+
mockStore
543+
.pipe(select(selector2))
544+
.subscribe(result => expect(result).toBe(1));
545+
mockStore
546+
.pipe(select(selector3))
547+
.subscribe(result => expect(result).toBe(6));
548+
});
549+
550+
it('should allow you reset mocked selectors', () => {
551+
const mockValue = 5;
552+
const selector = createSelector(
553+
() => initialState,
554+
state => state.counter1
555+
);
556+
const selector2 = createSelector(
557+
() => initialState,
558+
state => state.counter2
559+
);
560+
const selector3 = createSelector(
561+
selector,
562+
selector2,
563+
(sel1, sel2) => sel1 + sel2
564+
);
565+
566+
mockStore
567+
.pipe(select(selector3))
568+
.subscribe(result => expect(result).toBe(1));
569+
570+
mockStore.overrideSelector(selector, mockValue);
571+
mockStore.overrideSelector(selector2, mockValue);
572+
selector3.release();
573+
574+
mockStore
575+
.pipe(select(selector3))
576+
.subscribe(result => expect(result).toBe(10));
577+
578+
mockStore.resetSelectors();
579+
selector3.release();
580+
581+
mockStore
582+
.pipe(select(selector3))
583+
.subscribe(result => expect(result).toBe(1));
584+
});
485585
});
486586

487587
describe('Meta Reducers', () => {

modules/store/src/selector.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { Selector, SelectorWithProps } from './models';
22

33
export type AnyFn = (...args: any[]) => any;
44

5-
export type MemoizedProjection = { memoized: AnyFn; reset: () => void };
5+
export type MemoizedProjection = {
6+
memoized: AnyFn;
7+
reset: () => void;
8+
setResult: (result?: any) => void;
9+
};
610

711
export type MemoizeFn = (t: AnyFn) => MemoizedProjection;
812

@@ -12,12 +16,14 @@ export interface MemoizedSelector<State, Result>
1216
extends Selector<State, Result> {
1317
release(): void;
1418
projector: AnyFn;
19+
setResult: (result?: Result) => void;
1520
}
1621

1722
export interface MemoizedSelectorWithProps<State, Props, Result>
1823
extends SelectorWithProps<State, Props, Result> {
1924
release(): void;
2025
projector: AnyFn;
26+
setResult: (result?: Result) => void;
2127
}
2228

2329
export function isEqualCheck(a: any, b: any): boolean {
@@ -52,14 +58,23 @@ export function defaultMemoize(
5258
let lastArguments: null | IArguments = null;
5359
// tslint:disable-next-line:no-any anything could be the result.
5460
let lastResult: any = null;
61+
let overrideResult: any;
5562

5663
function reset() {
5764
lastArguments = null;
5865
lastResult = null;
5966
}
6067

68+
function setResult(result: any = undefined) {
69+
overrideResult = result;
70+
}
71+
6172
// tslint:disable-next-line:no-any anything could be the result.
6273
function memoized(): any {
74+
if (overrideResult !== undefined) {
75+
return overrideResult;
76+
}
77+
6378
if (!lastArguments) {
6479
lastResult = projectionFn.apply(null, arguments);
6580
lastArguments = arguments;
@@ -82,7 +97,7 @@ export function defaultMemoize(
8297
return newResult;
8398
}
8499

85-
return { memoized, reset };
100+
return { memoized, reset, setResult };
86101
}
87102

88103
export function createSelector<State, S1, Result>(
@@ -563,6 +578,7 @@ export function createSelectorFactory(
563578
return Object.assign(memoizedState.memoized, {
564579
release,
565580
projector: memoizedProjector.memoized,
581+
setResult: memoizedState.setResult,
566582
});
567583
};
568584
}

modules/store/testing/src/mock_store.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@ import {
66
INITIAL_STATE,
77
ReducerManager,
88
Store,
9+
createSelector,
10+
MemoizedSelectorWithProps,
11+
MemoizedSelector,
912
} from '@ngrx/store';
1013
import { MockState } from './mock_state';
1114

1215
@Injectable()
1316
export class MockStore<T> extends Store<T> {
17+
static selectors = new Map<
18+
| string
19+
| MemoizedSelector<any, any>
20+
| MemoizedSelectorWithProps<any, any, any>,
21+
any
22+
>();
23+
1424
public scannedActions$: Observable<Action>;
1525

1626
constructor(
@@ -20,6 +30,7 @@ export class MockStore<T> extends Store<T> {
2030
@Inject(INITIAL_STATE) private initialState: T
2131
) {
2232
super(state$, actionsObserver, reducerManager);
33+
this.resetSelectors();
2334
this.state$.next(this.initialState);
2435
this.scannedActions$ = actionsObserver.asObservable();
2536
}
@@ -28,6 +39,59 @@ export class MockStore<T> extends Store<T> {
2839
this.state$.next(nextState);
2940
}
3041

42+
overrideSelector<T, Result>(
43+
selector: string,
44+
value: Result
45+
): MemoizedSelector<string, Result>;
46+
overrideSelector<T, Result>(
47+
selector: MemoizedSelector<T, Result>,
48+
value: Result
49+
): MemoizedSelector<T, Result>;
50+
overrideSelector<T, Result>(
51+
selector: MemoizedSelectorWithProps<T, any, Result>,
52+
value: Result
53+
): MemoizedSelectorWithProps<T, any, Result>;
54+
overrideSelector<T, Result>(
55+
selector:
56+
| string
57+
| MemoizedSelector<any, any>
58+
| MemoizedSelectorWithProps<any, any, any>,
59+
value: any
60+
) {
61+
MockStore.selectors.set(selector, value);
62+
63+
if (typeof selector === 'string') {
64+
const stringSelector = createSelector(() => {}, () => value);
65+
66+
return stringSelector;
67+
}
68+
69+
selector.setResult(value);
70+
71+
return selector;
72+
}
73+
74+
resetSelectors() {
75+
MockStore.selectors.forEach((_, selector) => {
76+
if (typeof selector !== 'string') {
77+
selector.release();
78+
selector.setResult();
79+
}
80+
});
81+
82+
MockStore.selectors.clear();
83+
}
84+
85+
select(selector: any) {
86+
if (MockStore.selectors.has(selector)) {
87+
return new BehaviorSubject<any>(
88+
MockStore.selectors.get(selector)
89+
).asObservable();
90+
}
91+
92+
return super.select(selector);
93+
}
94+
3195
addReducer() {
3296
/* noop */
3397
}

projects/example-app/src/app/auth/components/login-form.component.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TestBed, ComponentFixture } from '@angular/core/testing';
22
import { NO_ERRORS_SCHEMA } from '@angular/core';
3-
import { StoreModule, Store, combineReducers } from '@ngrx/store';
43
import { LoginFormComponent } from '@example-app/auth/components/login-form.component';
54
import { ReactiveFormsModule } from '@angular/forms';
65

projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`Login Page should compile 1`] = `
44
<bc-login-page
55
error$={[Function Store]}
66
pending$={[Function Store]}
7-
store={[Function Store]}
7+
store={[Function MockStore]}
88
>
99
<bc-login-form
1010
ng-reflect-pending="false"

projects/example-app/src/app/auth/containers/login-page.component.spec.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,36 @@ import { TestBed, ComponentFixture } from '@angular/core/testing';
22
import { MatInputModule, MatCardModule } from '@angular/material';
33
import { ReactiveFormsModule } from '@angular/forms';
44
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
5-
import { StoreModule, Store, combineReducers } from '@ngrx/store';
5+
import { Store } from '@ngrx/store';
66
import { LoginPageComponent } from '@example-app/auth/containers/login-page.component';
77
import { LoginFormComponent } from '@example-app/auth/components/login-form.component';
88
import * as fromAuth from '@example-app/auth/reducers';
99
import { LoginPageActions } from '@example-app/auth/actions';
10+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
1011

1112
describe('Login Page', () => {
1213
let fixture: ComponentFixture<LoginPageComponent>;
13-
let store: Store<fromAuth.State>;
14+
let store: MockStore<fromAuth.State>;
1415
let instance: LoginPageComponent;
1516

1617
beforeEach(() => {
1718
TestBed.configureTestingModule({
1819
imports: [
1920
NoopAnimationsModule,
20-
StoreModule.forRoot(
21-
{
22-
auth: combineReducers(fromAuth.reducers),
23-
},
24-
{
25-
runtimeChecks: {
26-
strictImmutability: true,
27-
},
28-
}
29-
),
3021
MatInputModule,
3122
MatCardModule,
3223
ReactiveFormsModule,
3324
],
3425
declarations: [LoginPageComponent, LoginFormComponent],
26+
providers: [provideMockStore()],
3527
});
3628

3729
fixture = TestBed.createComponent(LoginPageComponent);
3830
instance = fixture.componentInstance;
3931
store = TestBed.get(Store);
32+
store.overrideSelector(fromAuth.getLoginPagePending, false);
4033

41-
spyOn(store, 'dispatch').and.callThrough();
34+
spyOn(store, 'dispatch');
4235
});
4336

4437
/**

0 commit comments

Comments
 (0)