How to test nested firestore batch functions in redux saga using jest? - firebase

In a react project I have a redux-saga file which I create and save new items on firebase backend(firestore).
In that saga function, I am getting a new write batch object first, then I update the firestore document, and finally I commit the batch.
Saga Worker
import { call, put } from 'redux-saga/effects'
import { db } from './firebase' // db: firebase.firestore()
export function* mySaga(item) {
try {
// init firestore batch.
const batch = yield call(db, db.batch)
// Set firestore document and save new item.
const itemRef = yield call ([db, db.doc], `/items/${item.id}`)
yield call([batch, batch.set], itemRef , item)
// Commit the batch.
yield call([batch, batch.commit])
yield put({type: 'success'})
} catch (err) {
yield put({type: 'error', payload: err})
}
}
Saga Worker's Test
import * as sagas from './mySaga'
describe('mySaga', () => {
const spyOnDoc = jest.spyOn(db, 'doc')
it('handles item creation', async () => {
const dispatched = []
await runSaga(
{ dispatch: action => dispatched.push(action) },
sagas.mySaga,
).toPromise()
expect(spyOnDoc).toHaveBeenCalledTimes(1)
// !!! Here I need to check for nested set and commit functions of the batch object created in saga.
})
})
How can I test the batch function's nested "set" and "commit" functions to check if they are called x times and called with proper inputs?
Any help would be appreciated.

After several attemps I figured out a way to accomplishing this kind of tests. In case if someone needs this solution, here it is.
db.batch() method creates a firebase.firestore.WriteBatch object. And this object has commit, set, update and delete methods. More details can be found here.
Final Saga Worker's Test
import * as sagas from './mySaga'
import { db } from './firebase' // db: firebase.firestore()
describe('mySaga', () => {
const spyOnDoc = jest.spyOn(db, 'doc')
// We are mocking the methods of this predefined object.
firestore.WriteBatch.set = jest.fn()
firestore.WriteBatch.commit = jest.fn()
// Then we implement those created mocks into the batch's mock implementation.
const spyOnBatch = jest.spyOn(db, 'batch').mockImplementation(() => ({
set: firestore.WriteBatch.set,
commit: firestore.WriteBatch.commit,
}))
it('handles item creation', async () => {
const dispatched = []
await runSaga(
{ dispatch: action => dispatched.push(action) },
sagas.mySaga,
{id: 123} // Item
).toPromise()
expect(spyOnDoc).toHaveBeenCalledTimes(1)
// Finally, we can test those nested object functions as below.
expect(firestore.WriteBatch.commit).toHaveBeenCalledTimes(1)
expect(firestore.WriteBatch.set).toHaveBeenCalledTimes(1)
expect(firestore.WriteBatch.set).toHaveBeenCalledWith(db.doc('/items/123'), {id: 123})
})
})

Related

How to update two databases reference with a single trigger function in firebase RTDB?

Let's say we have firebase project in which we have to use RTDB.
In RTDB we have created multiple databases.
I created a cloud trigger function i.e .onCreate so that my both databases get updated whenever I update any of two. When I am creating anything in default database it is working completely fine but when I am trying to update through other database (other than default one) it doesn't update default one. So could you please help me on this?
/* eslint-disable */
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
//this method is updating on creating data on database mentioned in instance id
export const newTest1=functions.database.instance('flysample-75b81-227ae').ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
//this method is updating only by creating data on default database
export const newTest2=functions.database.ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
//below 2 method works fine but i want to do this by single function
export const myFunTest1 = functions.database.instance('flysample-75b81').ref('/name')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
export const myFunTest2 = functions.database.instance('flysample-75b81-227ae').ref('/name')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
Your code is completely ignoring the asynchronous nature of writing to the database, which means there is no guarantee that any of the database writes completes before the instance gets terminated.
To ensure the writes don't get interrupted, wait for them to complete before returning a result with something like this:
export const newTest2=functions.database.ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
return Promise.all([
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
]).then(() => {
return "done";
});
});

Redux - Update store with same function from different files

being rather new to react.js + redux, I'm facing the following conundrum:
I have multiple files, which need to update the store in exactly the same way, based on the stores current state. Currently I simply copy-paste the same code (along with the needed mapStateToProps), which goes again DRY.
Similar to something like the below, where getData is an Ajax call living in the actions file and props.timeAttribute is coming from mapStateToProps:
props.getData(props.timeAttribute).then((newState) => {
console.log(newState)
})
Would a function like that go in the actions file? Can the current state be read from within that actions file? Or does one normally create some sort of helperFile.js in which a function like that lives and is being called from other files?
Thanks!
If your file is executing the same action, then yes, you would put the action creator in a separate file and export it. In theory, you can put state in an action by passing the state as a parameter, but the philosophy behind an action is that it announces to your application that SOMETHING HAPPENED (as denoted by the type property on the return value of the action function). The reducer function responsible for handling that type subsequently updates the state.
You can access the current state of the store inside of an action creator like this:
export const testAction = (someParam) => {
return (dispatch, getState) => {
const {
someState,
} = getState(); //getState gets the entire state of your application
//do something with someState and then run the dispatch function like this:
dispatch(() => {type: ACTION_TYPE, payload: updatedState})
}
I like this approach because it encapsulates all the logic for accessing state inside of the one function that will need to access it.
DO NOT modify the state inside of the action creator though! This should be read only. The state of your application should only be updated through your reducer functions.
Yes, it is recommended to maintain a separate file for your actions.
Below is an example of how i use an action to fetch information and dispatch an action.
export const fetchComments = () => (dispatch) => {
console.log("Fetch Comment invoked");
/*you can use your Ajax getData call instead of fetch.
Can also add parameters if you need */
return fetch(baseUrl + 'comments')
.then(response => {
if (response.ok){
return response;
}
else {
var error = new Error('Error ' + response.status + ': ' + response.statusText);
error.response = response;
throw error;
}
},
error => {
var errmess = new Error(error.message);
throw errmess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
}
/* Maintain a separate file called ActionTypes.js where you can store all the ActionTypes as Strings. */
export const addComments = (comments) => ({
type : ActionTypes.ADD_COMMENTS,
payload : comments
});
export const comments = (errMess) => ({
type : ActionTypes.COMMENTS_FAILED,
payload : errMess
});
Once, you receive dispatch an action, you need an reducer to capture the action and make changes to your store.
Note that this reducer must be a pure function.
export const comments = (state = { errMess: null, comments:[]}, action) => {
console.log("inside comments");
switch (action.type) {
case ActionTypes.ADD_COMMENTS:
return {...state, errMess: null, comments: action.payload};
case ActionTypes.COMMENTS_FAILED:
return {...state, errMess: action.payload};
default:
return state;
}
};
Don't forget to combine the reducers in the configureStore().
const store = createStore(
combineReducers({
comments
}),
applyMiddleware(thunk,logger)
);
In your components where you use the Actions, use
const mapDispatchToProps = dispatch => ({
fetchComments : () => dispatch(fetchComments()),
})
Note to export the component as
export default connect(mapStateToProps,mapDispatchToProps)(Component);

When to save the state tree?

The Redux manual says every reducer should be a pure function and even no API call should be made, I then curious to know, then, when should I get chance to save my App state tree to an external storage or the backend?
You can save your redux store using and action with the Redux Thunk middleware.
Lets say you want to want to save the store when the user clicks save. First, define an action to do the save:
actions/save.js
import fetch from 'isomorphic-fetch'
export const save = state => {
return () => {
fetch('/api/path/to/save', {
body: JSON.stringify(state),
headers: {
'content-type': 'application/json'
}
method: 'POST'
}
}
}
Then in your component:
components/SaveButton.js
import React from 'react'
import { connect } from 'react-redux';
import { save } from '../actions/save'
const SaveButton = props => {
let { onSave, state } = props
return <button onClick={onSave(state)}>Save</button>
}
const mapStateToProps = state => {
return {state}
}
const mapDispatchToProps = dispatch => {
return {
onSave: state => dispatch(save(state))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveButton)
You shouldn't do that as part of your reducer.
Instead, whenever you want to save some part of your state, you should dispatch an asynchronous action (with the help of middleware like redux-thunk) perhaps called SAVE_XYZ with it's payload being the part of the store you want to save.
dispatch(saveXYZ(data))
saveXYZ needs to be an async action creator that will dispatch the API call to persist your data, and handle the response accordingly.
const saveXYZ = payload => dispatch => {
dispatch(saveXYZPending());
return apiCallToStore(...)
.then(data => saveXYZDone())
.catch(err => saveXYZError());
}
You can read more on async actions and how to handle them.
Two basic approaches:
Use store.subscribe(callback), and write a callback that gets the latest state and persists it after some action has been dispatched
Write a middleware that persists the state when some condition is met
There's dozens of existing Redux store persistence libraries available that will do this work for you.

Perform Ajax Fetch in a Redux Reducer?

I'm trying to wrap my head around accessing the state inside Redux actionCreators; instead did the following (performed ajax operation in the reducer). Why do I need to access the state for this — because I want to perform ajax with a CSRF token stored in the state.
Could someone please tell me if the following is considered bad practice/anti-pattern?
export const reducer = (state = {} , action = {}) => {
case DELETE_COMMENT: {
// back-end ops
const formData = new FormData();
formData.append('csrf' , state.csrfToken);
fetch('/delete-comment/' + action.commentId , {
credentials:'include' ,
headers:new Headers({
'X-Requested-With':'XMLHttpRequest'
}) ,
method:'POST' ,
body:formData
})
// return new state
return {
...state ,
comments:state.comments.filter(comment => comment.id !== action.commentId)
};
}
default: {
return state;
}
}
From the redux documentation:
The only way to change the state is to emit an action, an object describing what happened. Do not put API calls into reducers. Reducers are just pure functions that take the previous state and an action, and return the next state. Remember to return new state objects, instead of mutating the previous state.
Actions should describe the change. Therefore, the action should contain the data for the new version of the state, or at least specify the transformation that needs to be made. As such, API calls should go into async actions that dispatch action(s) to update the state. Reducers must always be pure, and have no side effects.
Check out async actions for more information.
An example of an async action from the redux examples:
function fetchPosts(subreddit) {
return (dispatch, getState) => {
// contains the current state object
const state = getState();
// get token
const token = state.some.token;
dispatch(requestPosts(subreddit));
// Perform the API request
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
// Then dispatch the resulting json/data to the reducer
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
As per guidelines of redux.
It's very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().
If you are asking whether it is anti-pattern or not then yes it is absolutely.
But if you ask what is the solution.
Here you need to dispatch async-action from your action-creators
Use "redux-thunk" or "redux-saga" for that
You can access the state and create some async action
e.g inside your action-creator ( Just for example )
export function deleteCommment(commentId) {
return dispatch => {
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}
export function updateCommentList(commentList) {
return {
type : UPDATE_COMMENT_LIST,
commentList
};
}
Edit: You can access the state -
export function deleteCommment(commentId) {
return (dispatch, getState) => {
const state = getState();
// use some data from state
return Api.deleteComment(commentId)
.then( res => {
dispatch(updateCommentList(res));
});
};
}

Redux - Jest: Testing functions that have void return

New to Jest and Redux and I'm having trouble with testing functions that are dispatching to the store but don't yield a return value. I'm trying to follow the example from the Redux website does this
return store.dispatch(actions.fetchTodos()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions)
})
however I have several "fetchtodos" functions that don't return anything which causes the error TypeError:
Cannot read property 'then' of undefined due to returning undefined
I'm wondering what I can do to test that my mock store is correctly updating. Is there a way to dispatch the function, wait for it to finish and then compare the mock store with expected results?
Thanks
Edit: We're using typescript
action from tsx
export function selectTopic(topic: Topic | undefined): (dispatch: Redux.Dispatch<TopicState>) => void {
return (dispatch: Redux.Dispatch<TopicState>): void => {
dispatch({
type: SELECT_Topic,
payload: topic,
});
dispatch(reset(topic));
};
}
test.tsx
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('Select Topic action', () => {
it('should create an action to select .', () => {
const topic: Topic = mockdata.example[0];
const expectedAction = {
type: actions.SELECT_TOPIC,
payload: topic,
};
const store = mockStore(mockdata.defaultState);
return store.dispatch(actions.selectTopic(topic)).then(() => {
expect(store.getState()).toEqual(expectedAction);
});
});
});
The action is what I'm given to test(and there are many other functions similar to it. I'm getting that undefined error when running the test code, as the function isn't returning anything.
In Redux, the store's dispatch method is synchronous unless you attach middleware that changes that behavior, ie: returns a promise.
So this is likely a redux configuration problem. Be sure you are setting up your test store with the same middleware that allows you to use the promise pattern in production.
And as always, be sure to mock any network requests to avoid making api calls in test.

Resources