-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Mock Store #915
Comments
Great suggestion! @brandonroberts and I have a mock Store implementation we use internally that works similarly to how you propose. It should also setup |
Great proposal! IMHO will be nice to have a separate package inside platform with all testing helper tools. |
Yeah, We really need it! |
This has been raised a number of times already and I'd like to get the consensus whether mock Store is needed or not :) E.g. in ngrx/store#128 @robwormald says:
The same recommendation is at current testing guide for the store https://github.com/ngrx/platform/blob/master/docs/store/testing.md#providing-store-for-testing
Internally at Google I've been recommending to avoid mocking the Store as well - my suggestion was to use the real Store and if it's needed to be in certain state for the tests to just dispatch the sequence of actions. But we are using |
@alex-okrushko I am perfectly aware of the fact the store is synchronous. I believe that unit tests should just verify very narrow scope of reality and we shouldn't really need to build boilerplate code to make them run, right? |
@alex-okrushko We can't all be Rob Wormalds. 😉 Whether a mock store is necessary or unnecessary, I think at the very least, the documentation for unit testing around ngrx should define a much broader scope than is currently provided in the testing guide. The attraction for a mock store for me is ease of use and simplicity. If there is a mock that can be provided in my spec classes that reduces the friction for setting up spies, establishing needed mock state, and testing with effects easier, count me in. Maybe I'm lazy, or I don't understand the inner workings of ngrx sufficiently, but I'd like to make the task of writing tests near brainless. Don't make me think too hard just to scaffold out tests. Short of providing a mock, better documentation would be great around different scenarios. If that documentation exists... a more obvious link would be great. |
I'd appreciate a complete testing guide. Testing every part of the store (including router state) AND inclusion in components throught selectors. Testing documention is almost non existent. |
It's not a full-fledged MockStore, but it's helped quite a bit when I was testing container components that were subscribed via different store selectors. We had issues where we had to initialise nested state in the TestBed, and we already had separate selector tests to cover the selector logic. The MockStore implementation I wrote allows manual publishing of events per selector, which was useful in testing different permutations in the container component.
Here are the links to the implementation (my own sandbox repo outside of work). Hope this helps while an official MockStore is being worked on! |
I am doing it the way you are describing by triggering actions... i don't feel this is the right way (it's eurkk ). Example: (why i am saying this way is eurkk ^^!!) I think from what i see in @TeoTN or @honsq90 propositions, there are benefits of mocking directly the store/selectors vs using actions:
@TeoTN |
@honsq90 Solution however will not work on the new pipeable |
@nasreddineskandrani I rely on the fact that State is actually a Subject. I want to be extra clear on that: it's a dead simple, hammer-like solution. But I'm open for extensions ;) |
so @TeoTN can we build a solution to mock selector directly. It's more powerfull, No? |
@nasreddineskandrani that's an interesting idea but what if someone's not using the selector? I mean, it's a layer above the single source of truth and technically one may decide not to use it |
@nasreddineskandrani I've trying to mock the selectors for a whole day without any success. |
@nasreddineskandrani lol, that's so inclusive approach |
Joining the discussion here after I upgraded a huge project to use the pipeable operator. I've made my own implementation of export class MockStore<T extends RootState = RootState> extends BehaviorSubject<T> {
dispatch = jasmine.createSpy();
} But realised that it is far from ideal when dealing with unit tests (whereas it's just fine for integration tests where you provide the initial state). For unit tests, I want to be able to return directly a value for a given selector. So with the help of @zakhenry I came up with the following: export class MockStore<StateType extends RootState = RootState> extends BehaviorSubject<StateType> {
private selectorsToValues: Map<(...args: any[]) => any, any> = new Map();
public dispatch = jasmine.createSpy();
constructor(initialState: StateType = null, private returnNullForUnhandledSelectors = true) {
super(initialState);
spyOnProperty(ngrx, 'select').and.callFake(_ => {
return selector => {
let obs$: Observable<any>;
if (this.selectorsToValues.has(selector)) {
const value = this.selectorsToValues.get(selector);
obs$ = value instanceof Observable ? value : this.pipe(map(() => value));
}
obs$ = this.pipe(map(() => (this.returnNullForUnhandledSelectors ? null : selector(this.getValue()))));
return () => obs$.pipe(distinctUntilChanged());
};
});
}
addSelectorStub<T>(cb: (...args: any[]) => T, mockedValue: T | Observable<T>): this {
this.selectorsToValues.set(cb, mockedValue);
return this;
}
setState(state: StateType): this {
this.next(state);
return this;
}
setReturnNullForUnandledSelectors(value: boolean): this {
this.returnNullForUnhandledSelectors = value;
return this;
}
} It's very fresh and might need some more work. Also, notice the
|
@maxime1992 Nice |
@Juansasa took me a while to figure that out too. Had no idea it existed. Good thing is, I've learned quite a few things in the research process! With It seems that the default is setting the |
Is there any equivalent on jest? |
@blackholegalaxy maybe phra/jest@1a3b82a @maxime1992 can you please provide an example of your mockstore on stackblitz? |
@maxime1992 nice! Does it work universally with the traditional and also pipeable select? |
@maxime1992 I really like your implementation, I have added the store() function to your MockStore code, so is should work with the classic as well as pipeable select operator. and I have wrapped this piece of code in the else clause: Now it looks like (code based on code of @maxime1992 from few posts earlier): import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { AppState } from '@app/reducers.index';
import { map, distinctUntilChanged } from 'rxjs/operators';
import * as ngrx from '@ngrx/store';
@Injectable()
export class MockStore<StateType extends AppState = AppState> extends BehaviorSubject<StateType> {
private selectorsToValues: Map<(...args: any[]) => any, any> = new Map();
public dispatch = jasmine.createSpy();
public select = jasmine.createSpy().and.callFake(
(selector: any): Observable<any> => {
return this.getObservableWithMockResult(selector).pipe(distinctUntilChanged());
}
);
constructor(initialState: StateType = null, private returnNullForUnhandledSelectors = true) {
super(null);
spyOnProperty(ngrx, 'select').and.callFake(_ => {
return selector => {
return () => this.getObservableWithMockResult(selector).pipe(distinctUntilChanged());
};
});
}
private getObservableWithMockResult(selector:any):Observable<any>{
let obs$: Observable<any>;
if (this.selectorsToValues.has(selector)) {
const value = this.selectorsToValues.get(selector);
obs$ = value instanceof Observable ? value : this.pipe(map(() => value));
} else {
obs$ = this.pipe(map(() => (this.returnNullForUnhandledSelectors ? null : selector(this.getValue()))));
}
return obs$;
}
addSelectorStub<T>(cb: (...args: any[]) => T, mockedValue: T | Observable<T>): this {
this.selectorsToValues.set(cb, mockedValue);
return this;
}
setState(state: StateType): this {
this.next(state);
return this;
}
setReturnNullForUnandledSelectors(value: boolean): this {
this.returnNullForUnhandledSelectors = value;
return this;
}
} I am using it in tests like this at the moment: import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { StoreModule, Store } from '@ngrx/store';
import { MockStore } from '@app/test-utils/mockup-store';
describe('SomeComponent', () => {
let component: SomeComponent;
let fixture: ComponentFixture<SomeComponent>;
let mockStore: MockStore<ExtendedStateWithLazyLoadedFeatures>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SomeComponent],
imports: [StoreModule.forRoot({})],
providers: [{ provide: Store, useValue: new MockStore<ExtendedStateWithLazyLoadedFeatures>(null, true) }]
}).compileComponents();
}));
beforeEach(inject([Store], (testStore: MockStore<ExtendedStateWithLazyLoadedFeatures>) => {
// save the automatically injected value so we can reference it in later tests
mockStore = testStore;
}));
describe('with values for selectors defined', () => {
beforeEach(() => {
mockStore.addSelectorStub(selector1, {});
mockStore.addSelectorStub(selector2, ["mockValue"]);
});
beforeEach(() => {
fixture = TestBed.createComponent(SomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// this is checking if the non-pipeable variant of select was called
it('should call selector selector1', () => {
expect(mockStore.select).toHaveBeenCalledWith(selector1);
});
// this is checking if the non-pipeable variant of select was called
it('should call selector selector2', () => {
expect(mockStore.select).toHaveBeenCalledWith(selector2);
});
});
}); I am a beginner with Angular tests. Feedback is appreciated. |
@SerkanSipahi you need to build the selector with |
I checked the online example. I took an example which is not built with pipe (it works in online example): luffy.selector.ts
luffy.component.spec.js
Error:
|
@SerkanSipahi GG! i assumed it's not working without pipe but it's even more simple 💃 so why are we trying to mock With my friends, we didn't try this at first place following the doc with action triggers. But it was none productive that's why i was hunting a better solution. So i guess an update is needed with this way of mocking selectors into the ngrx testing documentation. For the doc update:
Question: this kill the need of mocking |
I was really exited about that we getting mock store, selector, etc but i think its not ready for usage its makes more complicated. I dont know. Well, mocking selector and store are not worked well for me or just lets say: really hard to do that simple (maybe im stupid) or maybe it is not fully developed yet, i dont know! For me a test setup should be very easy without or with less think about it. For me it workes very well and easy to test my effect with the real store as @alex-okrushko suggested! effect:
effect.spec:
Now im not agree with @TeoTN of:
|
with @TeoTN proposal you can set a state in the store without triggering actions. In your case, you are not triggering actions to set a state. But if you had to trigger actions @TeoTN proposal is better. |
But i have to triggering a action. How should i catch
See im triggering a action(s) and setting a new state:
It did not worked for me. I can just set a state with |
Sorry i didn't see it :) you need to trigger the effect. This is the action you can avoid I already explained one case with common component where triggering actions isn't welcome i ll give you another case related to your example, *this is not common for me to see 'dom' stuff in effect ( ), you can poke me in gitter in private if you wanna talk about this or ask in gitter/ngrx. I built multi app with redux react/angular never had to do that. |
@SerkanSipahi can you post the full example how you made it work with a selector which is not wrapped? I tried it in the example and I get an error. https://stackblitz.com/edit/ngrx-component-testing-simplified-7fd5nk?file=src/app/luffy/luffy.component.spec.ts |
@marcelnem i tried it it's not working for me without |
@marcelnem @nasreddineskandrani I think here is a misunderstanding! What i meant was, its not possible to compile that thing when its not wrapped by pipe! I add When i do that with my selectors to spyOn a selector which is not wrapped by pipe i get an error. Which version of ngrx you are using? |
@nasreddineskandrani thank you for your good explanation! Now i got it. It took a while until I understood it (it was to much: mock.selector, mock.next, marble tests, etc. etc) For just clarification(to improve my understanding): in my case it make no sense to mock the store (nextMock), right? Or is there something what i not see? maybe, imagine |
Update: I left some feedback on the PR for the proposed testing API for Store here #1027 (comment) Leave feedback if you have any here and not there. |
@brandonroberts some argumentations on why, in my opinion, mocking selectors could be good also!
From your statement, i wanted to say that in my case i don't want to test selectors when i want to unit test components. why? Because integration tests are slower and should be fewer than unit tests to fail faster. Also there is a way the conclusion section of article:
By your statement you don't provide a way to unit test components (doc update or code update) and encourage integration tests with real selectors to test components. i know :) i am taking it far but another little point, when it fails you know it's in the component not the selector => More isolated. thank you for the answer and i am happy |
@nasreddineskandrani I see what you are saying. If you want to unit test a component that has selectors, override the class variables with test observables. Here is a simple example of overriding the selector itself, versus setting the state and using the selector as-is. https://stackblitz.com/edit/ngrx-test-template?file=app%2Fcounter%2Fcounter.component.spec.ts |
thank you very much so simple 👍 |
@maxime1992 responding to your comment in the PR - #1027 (comment). While your solution works, it's bound to |
@timdeschryver that's a fair point. That said, I don't really like the idea of replacing directly the component's variable:
|
Those are valid points :) |
@maxime1992 I don't disagree with those points, but I don't think we should use a Jasmine/Jest specific way to mock selectors. Using I haven't tested this, but selectors could include an API you can access on the selector itself similar to |
Here is a newer version that lets you override selectors in addition to state. It adds a method to the returned selector to let you set the result value that will be returned. import { Injectable, Inject, OnDestroy } from '@angular/core';
import {
Store,
ReducerManager,
ActionsSubject,
ActionReducer,
Action,
ScannedActionsSubject,
INITIAL_STATE,
MemoizedSelector,
MemoizedSelectorWithProps,
} from '@ngrx/store';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class MockState<T> extends BehaviorSubject<T> {
constructor() {
super({} as T);
}
}
@Injectable()
export class MockReducerManager extends BehaviorSubject<
ActionReducer<any, any>
> {
constructor() {
super(() => undefined);
}
}
@Injectable()
export class MockStore<T> extends Store<T> {
static selectors = new Set<
MemoizedSelector<any, any> | MemoizedSelectorWithProps<any, any, any>
>();
constructor(
private state$: MockState<T>,
actionsObserver: ActionsSubject,
reducerManager: ReducerManager,
public scannedActions$: ScannedActionsSubject,
@Inject(INITIAL_STATE) private initialState: T
) {
super(state$, actionsObserver, reducerManager);
this.resetSelectors();
this.state$.next(this.initialState);
}
setState(state: T): void {
this.state$.next(state);
}
overrideSelector<T, Result>(
selector: MemoizedSelector<T, Result>,
value: Result
): MemoizedSelector<T, Result>;
overrideSelector<T, Result>(
selector: MemoizedSelectorWithProps<T, any, Result>,
value: Result
): MemoizedSelectorWithProps<T, any, Result>;
overrideSelector<T, Result>(
selector:
| MemoizedSelector<any, any>
| MemoizedSelectorWithProps<any, any, any>,
value: any
) {
MockStore.selectors.add(selector);
selector.setResult(value);
return selector;
}
resetSelectors() {
MockStore.selectors.forEach(selector => selector.setResult());
MockStore.selectors.clear();
}
dispatch(action: Action) {
super.dispatch(action);
this.scannedActions$.next(action);
}
addReducer() {
// noop
}
removeReducer() {
// noop
}
}
export function provideMockStore<T>(config: { initialState?: T } = {}) {
return [
{ provide: INITIAL_STATE, useValue: config.initialState },
ActionsSubject,
ScannedActionsSubject,
MockState,
{ provide: ReducerManager, useClass: MockReducerManager },
{
provide: Store,
useClass: MockStore,
},
];
} If we take the import { TestBed, inject } from '@angular/core/testing';
import { Store, MemoizedSelector } from '@ngrx/store';
import { cold } from 'jasmine-marbles';
import { AuthGuard } from '@example-app/auth/services/auth-guard.service';
import { AuthApiActions } from '@example-app/auth/actions';
import * as fromRoot from '@example-app/reducers';
import * as fromAuth from '@example-app/auth/reducers';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: MockStore<any>;
let loggedIn: MemoizedSelector<fromAuth.State, boolean>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore()],
});
store = TestBed.get(Store);
guard = TestBed.get(AuthGuard);
loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false);
});
it('should return false if the user state is not logged in', () => {
const expected = cold('(a|)', { a: false });
expect(guard.canActivate()).toBeObservable(expected);
});
it('should return true if the user state is logged in', () => {
loggedIn.setResult(true);
const expected = cold('(a|)', { a: true });
expect(guard.canActivate()).toBeObservable(expected);
});
}); |
@brandonroberts getting: |
@brandonroberts I like some of the ideas from here and the PR. I was busy recently and didn't have time to incorporate them in the code, but I'll definitely take the time to improve that. Stay tuned ;) |
@TeoTN |
@brandonroberts Shouldn't be overrideSelector method merged? |
@tomasznastaly |
I'm submitting a...
What is the current behavior?
There's no simple way to mock a Store for testing purposes.
Expected behavior:
When testing a component or an effect, I'd like to be able to inject a fake Store to the tested entity. It became increasingly non-trivial after the pipable operators were introduced.
For instance, one may want to test behavior component with a given state that is a result of many various actions. Similarly, one may want to test an effect with various fixtures for state.
Across the web there are multiple snippets with custom implementation of
MockStore
that extends eitherStore
orBehaviorSubject
but all of them are outdated. The presence of such snippets suggests though that there's a real need for some testing utils.I propose introduction of
@ngrx/store/testing
module which would containprovideMockStore
for providingMockStore
in place ofStore
and an implementation ofMockStore
that providesnextMock
method that replaces current state just likeBehaviorSubject.next
does on Subject.Version of affected browser(s),operating system(s), npm, node and ngrx:
Newest ngrx, reasonably new npm, node, Chrome and Windows.
Other information:
I'll make a contribution with
MockStore
class and a provider, if you feel it's a valid issue.The text was updated successfully, but these errors were encountered: