Just new in react , react-redux/saga and jest
consider:
-----The Componnent ()----
componentDidMount() {
this.props.actions.initTodos(
axios,
ajaxURLConstants.WP_GET_TODOS,
appStateActions.setAppInIdle,
appStateActions.setAppInProcessing,
todosActions.todosInitialized
);
}
So when my TodoApp component did mount, it will dispatch the INIT_TODOS action which then my root saga is listening , and when it caught it, will spawn the appropriate worker saga to act accordingly.
-----The Corresponding Worker Saga-----
export function* initTodosSaga( action ) {
try {
yield put( action.setAppInProcessing() );
let response = yield call( action.axios.get , action.WP_GET_TODOS );
if ( response.data.status === "success" )
yield put( action.todosInitialized( response.data.todos ) );
else {
console.log( response );
alert( response.data.error_msg );
}
} catch ( error ) {
console.log( "error" , error );
alert( "Failed to load initial data" );
}
yield put( action.setAppInIdle() );
}
-----The Test So Far-----
import todos from "../../__fixtures__/todos";
import { initTodosSaga } from "../todosSaga";
test( "saga test" , () => {
let response = {
status : "success",
todos
},
action = {
axios : {
get : function() {
return new Promise( ( resolve , reject ) => {
resolve( response );
} );
}
},
WP_GET_TODOS : "dummy url",
setAppInIdle : jest.fn(),
setAppInProcessing : jest.fn(),
todosInitialized : jest.fn()
};
let initTodosSagaGen = initTodosSaga( action );
initTodosSagaGen.next();
expect( action.setAppInIdle ).toHaveBeenCalled();
} );
-----The Test Result-----
So the important part is this
console.error node_modules\redux-saga\lib\internal\utils.js:240
uncaught at check put(action): argument action is undefined
but I have console.log the action i passed on my test inside the worker saga and indeed it is not undefined
what am I missing?
Thanks in advance.
----------Update------------
Ok notice on the top that it is complaining on this line of code
yield put( action.setAppInIdle() );
Which is outside the try catch block , so i made a couple of changes
1.) I moved the code above inside the try catch block, just after the else statement of
if ( response.data.status === "success" )
please check initTodosSaga code above
Then on my saga test, i test for
expect( action.setAppInProcessing ).toHaveBeenCalled();
instead of the setAppInIdle spy function
and this is the test result
so the test passed! but still it is complaining about the action being undefined
now what is interesting is if in my saga test, if I test for this now
expect( action.setAppInProcessing ).toHaveBeenCalled();
expect( action.setAppInIdle ).toHaveBeenCalled();
This is the result
so now it still complains about the action still undefined ( I have not included in my screenshot, but still same as above )
plus the second assert i have about the setAppInIdle spy function was not called, but the setAppInProcessing did pass!
I hope this additional info helps in resolving this question.
Redux Saga is a middleware library that lets the Redux store interact with resources outside of itself asynchronously. It also handles asynchronous logic in applications. For example, Ajax calls, logging, accessing browsers local storage. It is also responsible for executing the effects that are yielded from a saga.
Step by Step approach Our sagas being generator functions always yield effects which are saga factory functions like takeEvery, put, call etc. We can test each yield statement one by one by calling them using next() and asserting the returned values to the expected effect.
Such a powerful & elegant tool as Redux-Saga, a Redux side effect manager, is said to be deprecated, and no longer being maintained, starting from Jan 27, 2021.
Here is a working version of your test:
import todos from '../../__fixtures__/todos';
import { initTodosSaga } from '../todosSaga';
import { put, call } from 'redux-saga/effects';
test('saga test', () => {
const response = {
data: {
status: 'success',
todos
}
};
const action = {
axios: {
get() {}
},
WP_GET_TODOS: 'dummy url',
setAppInIdle: jest.fn().mockReturnValue({ type: 'setAppInIdle' }),
setAppInProcessing: jest.fn().mockReturnValue({ type: 'setAppInProcessing' }),
todosInitialized: jest.fn().mockReturnValue({ type: 'todosInitialized' })
};
let result;
const initTodosSagaGen = initTodosSaga(action);
result = initTodosSagaGen.next();
expect(result.value).toEqual(put(action.setAppInProcessing()));
expect(action.setAppInProcessing).toHaveBeenCalled();
result = initTodosSagaGen.next();
expect(result.value).toEqual(call(action.axios.get, action.WP_GET_TODOS));
result = initTodosSagaGen.next(response);
expect(action.todosInitialized).toHaveBeenCalled();
expect(result.value).toEqual(put(action.todosInitialized(response.data.todos)));
result = initTodosSagaGen.next();
expect(action.setAppInIdle).toHaveBeenCalled();
expect(result.value).toEqual(put(action.setAppInIdle()));
});
Some notes:
expect
statements, I'm comparing the yield of the generator to what I expect the generator to do (i.e. execute put
and call
statements)data
property was missing from your mock responseIt seems it is very difficult to test redux saga without any aid of an external library
For me I used https://github.com/jfairbank/redux-saga-test-plan
This library is very good.
So here is my tests now
--------------------Test 1---------------------
So for this test, I passed along the action payload almost everything the saga needs for it to function, ex. axios , action creator functions, etc... more like following the principle of dependency injection so its easy to test.
-----TodoApp Component-----
componentDidMount() {
this.props.actions.initTodos(
axios,
ajaxURLConstants.WP_GET_TODOS,
appStateActions.setAppInIdle,
appStateActions.setAppInProcessing,
todosActions.todosInitialized,
todosActions.todosFailedInit
);
}
So when the component did mount it fires an action that my root saga listens and catches and then spawns the appropriate worker saga to act accordingly
again notice I pass along all necessary data that the worker saga would need to operate properly on the actions payload.
-----initTodoSaga (Worker Saga)-----
export function* initTodosSaga( action ) {
try {
yield put( action.setAppInProcessing() );
let response = yield call( action.axios.get , action.WP_GET_TODOS );
if ( response.data.status === "success" )
yield put( action.todosInitialized( response.data.todos ) );
else {
console.log( response );
alert( response.data.error_msg );
yield put( action.todosFailedInit( response ) );
}
} catch ( error ) {
console.log( "error" , error );
alert( "Failed to load initial data" );
yield put( action.todosFailedInit( error ) );
}
yield put( action.setAppInIdle() );
}
-----Saga Test-----
import { expectSaga } from "redux-saga-test-plan";
import { initTodosSaga } from "../todosSaga";
test( "should initialize the ToDos state via the initTodoSaga" , () => {
let response = {
data : {
status : "success",
todos
}
},
action = {
axios : {
get : function() {
return new Promise( ( resolve , reject ) => {
resolve( response );
} );
}
},
WP_GET_TODOS : "dummy url",
setAppInIdle : appStateActions.setAppInIdle,
setAppInProcessing : appStateActions.setAppInProcessing,
todosInitialized : todosStateActions.todosInitialized,
todosFailedInit : todosStateActions.todosFailedInit
};
// This is the important bit
// These are the assertions
// Basically saying that the actions below inside the put should be dispatched when this saga is executed
return expectSaga( initTodosSaga , action )
.put( appStateActions.setAppInProcessing() )
.put( todosStateActions.todosInitialized( todos ) )
.put( appStateActions.setAppInIdle() )
.run();
} );
and my test pass yay! :) now to show you the error message when a test fails, I will comment out this line of code in my initTodosSaga
yield put( action.setAppInIdle() );
so now the assertion
.put( appStateActions.setAppInIdle() )
should fail now
so it outputs put expectation unmet which makes sense as the action we expected to be fired didn't
--------------------Test 2--------------------
Now this test is for a saga in which it imports some things it needs to operate unlike my First test where I feed axios, action creators inside the action payload
This saga imported axios, action creators it needs to operate
Thankfully Redux Saga Test Plan have some helper functions to "feed" dummy data into the saga
I will just skip the component that fires the action that the root saga is listening, its not important, I will just paste directly the saga and the saga test
----addTodoSaga----
/** global ajaxurl */
import axios from "axios";
import { call , put } from "redux-saga/effects";
import * as appStateActions from "../actions/appStateActions";
import * as todosActions from "../actions/todosActions";
export function* addTodoSaga( action ) {
try {
yield put( appStateActions.setAppInProcessing() );
let formData = new FormData;
formData.append( "todo" , JSON.stringify( action.todo ) );
let response = yield call( axios.post , ajaxurl + "?action=wptd_add_todo" , formData );
if ( response.data.status === "success" ) {
yield put( todosActions.todoAdded( action.todo ) );
action.successCallback();
} else {
console.log( response );
alert( response.data.error_msg );
}
} catch ( error ) {
console.log( error );
alert( "Failed to add new todo" );
}
yield put( appStateActions.setAppInIdle() );
}
-----The Test-----
import axios from "axios";
import { expectSaga } from "redux-saga-test-plan";
import * as matchers from "redux-saga-test-plan/matchers";
import * as appStateActions from "../../actions/appStateActions";
import * as todosStateActions from "../../actions/todosActions";
import { addTodoSaga } from "../todosSaga";
test( "should dispatch TODO_ADDED action when adding new todo is successful" , () => {
let response = {
data : { status : "success" }
},
todo = {
id : 1,
completed : false,
title : "Browse 9gag tonight"
},
action = {
todo,
successCallback : jest.fn()
};
// Here are the assertions
return expectSaga( addTodoSaga , action )
.provide( [
[ matchers.call.fn( axios.post ) , response ]
] )
.put( appStateActions.setAppInProcessing() )
.put( todosStateActions.todoAdded( todo ) )
.put( appStateActions.setAppInIdle() )
.run();
} );
So the provide function allows you to mock a function call and at the same time provide dummy data that it should return
and that's it, I'm able to test now my sagas! yay!
one more thing, when I run a test for my saga that results in executing a code with alert code
ex.
alert( "Earth is not flat!" );
I got this on the console
Error: Not implemented: window.alert
and a bunch of stack trace below it, so maybe its because the alert object is not present on node? how do I hide this? just add on the comment if you guys have an answer.
I hope this helps anyone
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With