-
Notifications
You must be signed in to change notification settings - Fork 405
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
🚀[FEATURE]: How to mock dispatch #1007
Comments
@uiii hi, could you please help me with your idea. |
@splincode Hi, perhaps, what do you need to know? |
My intention to report this issue was I started creating unit tests. But I'm not skilled at it and work with ngxs only few weeks. As I studied, I've found out unit test should really test only one unit of code and all dependencies shloud be mocked as much as possible. This make sense to me. What I didn't like at action testing as described here: https://ngxs.gitbook.io/ngxs/recipes/unit-testing is that the store is set up and the action is actually dispatched which may cause side-effects so it is more like integration testing. Few days ago I've found new way how to accomplish clean unit testing and it is mocking the action context and run the action handler directly without dispatching the action: let indexState: IndexState
let indexStateContextMock: StateContextMock<IndexStateModel>
beforeEach(() => {
indexState = new IndexState(indexServiceMock)
indexStateContextMock = mockStateContext<IndexStateModel>(IndexState)
})
it('should fetch modules', async (done) => {
indexState.fetchModules(indexStateContextMock, new FetchModules).subscribe(() => {
await expect(IndexState.getPending(indexStateContextMock.getState())).toBeTruthy()
await expect(indexStateContextMock.dispatch).toHaveBeenCalledWith(new ModulesFetched(fetchedModulesFixture))
done()
})
}) the mock helpers are this: export type StateContextMock<T> = jasmine.SpyObj<StateContext<T>>
export function mockStateContext<T>(stateClass: any): StateContextMock<T> {
let values = stateClass['NGXS_OPTIONS_META'].defaults
return {
getState: jasmine.createSpy('getState').and.callFake(() => values),
setState: jasmine.createSpy('setState').and.callFake((val: T) => values = val),
patchState: jasmine.createSpy('setState').and.callFake((val: Partial<T>) => values = { ...values, ...val }),
dispatch: jasmine.createSpy('dispatch').and.callFake(() => of())
}
} I'm quite happy with it. There is one problem that I have to use the |
My colleague mentioned that with this I didn't test if the action decorator is correctly set with the appropriate action handler so it will be called by dispatching the action. I solved it with checking the action handler metadata. This is not possibly ideal because it relies on internal stuff but it works. export function getActionFunctions(stateClass: any, actionClass: any): Function[] {
return stateClass['NGXS_META'].actions[actionClass.type].map(meta => stateClass.prototype[meta.fn])
}
...
it('should have fetch modules action function', async () => {
await expect(getActionFunctions(IndexState, FetchModules)).toContain(indexState.fetchModules)
}) |
I'm not aware of what's going on in this topic. Could you describe more clearly what would you like to achieve? |
@arturovt Sorry man, but I'm not sure if I can describe it more. |
As I understand from the topic - your problem is the The IMHO you don't have to "test actions" but actually you have to test the result of action handlers. What I mean is - you dispatch an action, you do something inside action handler (e.g. set some state), then you select the snapshot if this state and test its value. |
@arturovt Yes, I'm testing the result of the action and as a result I consider the values in the state (or returned from selectors) and what other actions are dispatched. What I want is to not actually dispatch the other actions, because it could result in different state values. Consider you have one action for fetching data which sets a pending status to true and another action for storing the fetched data which set the pending status to false. You can't test if the pending status is correctly set to true if the second action is dispatched as well. I don't think is is neccessary to resolve my original request to be able to mock the What could be done is make it more convenient to provide tool to easily mock state context etc. |
This problem is now considered here: ngxs-labs/testing#1 |
With ngxs-labs/testing being archived, is there a plan for making this doable? |
As much as I love using NGXS, this seems to be a major downside. The catch of using redux state management should be the simplicity of testing. However, not being able to test dispatched actions from another action in a straight-forward way goes directly against this advantage. |
Here is a code snippet of a utility that I like to use for capturing actions that have been dispatched in my tests. You can either add it to your imports ( Example usage in the doc comments in the code. import {
Injectable,
ModuleWithProviders,
NgModule,
OnDestroy,
} from '@angular/core';
import { Actions } from '@ngxs/store';
import { ActionStatus } from '@ngxs/store/src/actions-stream';
import { ReplaySubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class NgxsActionCollector implements OnDestroy {
/**
* Including this in your imported modules will
* set up the the action collector to start collecting actions
* from before Ngxs initializes
* @example
* // In your module declaration for your tests:
* {
* imports: [
* NgxsActionCollector.collectActions(),
* NgxsModule.forRoot([MyState]),
* ],
* // ...
* }
* // and then in your test:
* const actionCollector = TestBed.inject(NgxsActionCollector);
* const actionsDispatched = actionCollector.dispatched;
* const action = actionsDispatched.find(
* (item) => item instanceof MyAction
* );
* expect(action).toBeDefined();
* @returns A module that starts the collector immediately
*/
public static collectActions(): ModuleWithProviders<any> {
@NgModule()
class NgxsActionCollectorModule {
constructor(collectorService: NgxsActionCollector) {
collectorService.start();
}
}
return {
ngModule: NgxsActionCollectorModule,
providers: [Actions, NgxsActionCollector],
};
}
private destroyed$ = new ReplaySubject<void>(1);
private stopped$ = new Subject<void>();
private started = false;
public readonly dispatched: any[] = [];
public readonly completed: any[] = [];
public readonly successful: any[] = [];
public readonly errored: any[] = [];
public readonly cancelled: any[] = [];
constructor(private actions$: Actions) {}
start() {
if (this.started) {
return;
}
this.started = true;
this.actions$
.pipe(takeUntil(this.destroyed$), takeUntil(this.stopped$))
.subscribe({
next: (actionCtx: { status: ActionStatus; action: any }) => {
switch (actionCtx?.status) {
case ActionStatus.Dispatched:
this.dispatched.push(actionCtx.action);
break;
case ActionStatus.Successful:
this.successful.push(actionCtx.action);
this.completed.push(actionCtx.action);
break;
case ActionStatus.Errored:
this.errored.push(actionCtx.action);
this.completed.push(actionCtx.action);
break;
case ActionStatus.Canceled:
this.cancelled.push(actionCtx.action);
this.completed.push(actionCtx.action);
break;
default:
break;
}
},
complete: () => {
this.started = false;
},
error: () => {
this.started = false;
},
});
}
reset() {
function clearArray(arr) {
arr.splice(0, arr.length);
}
clearArray(this.dispatched);
clearArray(this.completed);
clearArray(this.successful);
clearArray(this.errored);
clearArray(this.cancelled);
}
stop() {
this.stopped$.next();
}
ngOnDestroy(): void {
this.destroyed$.next();
}
} |
Hi Mark, Many thanks for sharing NgxsActionCollector here - it's proved to be useful in our tests. Out of interest - can this be used in production code as well as test code or may there be reasons you wouldn't recommend it? Also, would it make sense to package this into a a standalone npm package or add it to the ngxs-store package? |
Is there any update on this subject since 2 years ? |
Hi, I'm creating unit tests and have a problem when testing actions. I would like to mock
dispatch
function in a way it really dispatches only a tested action and any additional dispatch call are only spied.Something like this:
BUT! This doesn't work, because as I investigate, the
store.dispatch
is different from thedispatch
function in the action context. I know I can use the construction like this without mocking (and it works):BUT! I don't want to actually dispatch additional actions because of side effects. Consider the tested action dispatches an action from another module, so I would have to mock all services which causes side effects in that module.
I've found out the actual dispatch to be mocked is the one in the
InternalStateOperations
object, but I don't know how to mock it.QUESTION So what is the proper way to make tests like this?
The text was updated successfully, but these errors were encountered: