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.
It 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