Test Firebase RealtimeDatabase using Jest - javascript

My purpose is simply to test one function. I cannot figure out how to mock firebase properly. I try to keep the example with axios mocking from Jest docs. I have the following code:
MusicService.js
import { initializeApp } from "firebase/app";
import "firebase/database";
const firebase = initializeApp({
apiKey: "<API_KEY>",
authDomain: "<PROJECT_ID>.firebaseapp.com",
databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
projectId: "<PROJECT_ID>",
storageBucket: "<BUCKET>.appspot.com",
messagingSenderId: "<SENDER_ID>",
});
export class MusicService {
static getAlbums() {
return firebase.database().ref("albums").once("value")
.then(snapshot => Object.values(snapshot.val()));
}
}
MusicService.test.js
import firebase from 'firebase/app';
import 'firebase/database';
import { MusicService } from './MusicService';
jest.mock('firebase/app');
jest.mock('firebase/database');
test("test", () => {
firebase.initializeApp.mockImplementation(() => {
database: jest.fn(() => {
return {
ref: jest.fn()
}
})
});
MusicService.getAlbums();
});
The problem is that I get the following error:
I tried to mock firebase.database.
test("test", () => {
firebase.mockImplementation(() => {
return {
database: {
}
}
});
MusicService.getAlbums();
});
But in this case I get the error that says:
TypeError: _app.default.mockImplementation is not a function.
I don't expect the working example will be given, but rather could you tell please, what exactly should I mock? The whole firebase library or maybe the part where my function starts - return firebase.database().

I have figured out. I should mock only those modules, a function I am going to test, depends on. For example, I want to test getAlbums function. It uses initializeApp function which is imported from firebase/app module in MusicService.js. So when initializeApp function is being called it should return an object containing database function which in turn returns an object with ref and once functions. Code:
MusicService.test.js.
import { MusicService } from "./FirebaseService";
jest.mock("firebase/app", () => {
const data = { name: "unnamed" };
const snapshot = { val: () => data };
return {
initializeApp: jest.fn().mockReturnValue({
database: jest.fn().mockReturnValue({
ref: jest.fn().mockReturnThis(),
once: jest.fn(() => Promise.resolve(snapshot))
})
})
};
});
test("getAlbums function returns an array", async () => {
const data = await MusicService.getAlbums();
expect(data.constructor).toEqual(Array);
});

this is my current mock implementation for firebase.js.
for me it is working fine.
const firebase = jest.genMockFromModule('firebase');
firebase.initializeApp = jest.fn();
const data = { name: 'data' };
const snapshot = { val: () => data, exportVal: () => data, exists: jest.fn(() => true) };
firebase.database = jest.fn().mockReturnValue({
ref: jest.fn().mockReturnThis(),
on: jest.fn((eventType, callback) => callback(snapshot)),
update: jest.fn(() => Promise.resolve(snapshot)),
remove: jest.fn(() => Promise.resolve()),
once: jest.fn(() => Promise.resolve(snapshot)),
});
firebase.auth = jest.fn().mockReturnValue({
currentUser: true,
signOut() {
return Promise.resolve();
},
signInWithEmailAndPassword(email, password) {
return new Promise((resolve, reject) => {
if (password === 'sign' || password === 'key') {
resolve({ name: 'user' });
}
reject(Error('sign in error '));
});
},
createUserWithEmailAndPassword(email, password) {
return new Promise((resolve, reject) => {
if (password === 'create' || password === 'key') {
resolve({ name: 'createUser' });
}
reject(Error('create user error '));
});
},
});
export default firebase;

Related

Not able to get the id of the generated firebase document

I'm trying to get the id of the generated firebase document, and I'm using addDoc to create a new doc.
I'm generating a new document on button click and that button calls the initializeCodeEditor function.
Anyone please help me with this!
Button Code:
import { useNavigate } from "react-router-dom"
import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"
import Button from "./Button"
const StartCodingButton = ({ document, setIsOpen }) => {
const { user } = useAuthContext()
const { addDocument, response } = useFirestore("solutions")
const navigate = useNavigate()
const initializeCodeEditor = async () => {
await addDocument({
...document,
author: user.name,
userID: user.uid,
})
if (!response.error) {
console.log(response.document) // null
const id = response?.document?.id; // undefined
navigate(`/solution/${id}`, { state: true })
}
}
return (
<Button
className="font-medium"
variant="primary"
size="medium"
onClick={initializeCodeEditor}
loading={response.isPending}
>
Start coding online
</Button>
)
}
export default StartCodingButton
addDocument code
import { useReducer } from "react"
import {
addDoc,
collection,
doc,
Timestamp,
} from "firebase/firestore"
import { db } from "../firebase/config"
import { firestoreReducer } from "../reducers/firestoreReducer"
const initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
export const useFirestore = (c) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
// add a document
const addDocument = async (doc) => {
dispatch({ type: "IS_PENDING" })
try {
const createdAt = Timestamp.now()
const addedDocument = await addDoc(collection(db, c), {
...doc,
createdAt,
})
dispatch({ type: "ADDED_DOCUMENT", payload: addedDocument })
} catch (error) {
dispatch({ type: "ERROR", payload: error.message })
}
}
return {
addDocument,
response,
}
}
firestoreReducer
export const firestoreReducer = (state, action) => {
switch (action.type) {
case "IS_PENDING":
return { isPending: true, document: null, success: false, error: null }
case "ADDED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
}
throw Error("Unknown action: " + action.type)
}
I have recreated this issue and found out this is happening because the response object in the useFirestore hook is not being updated until the next render cycle.
In order to get the updated response object, you can use the useEffect hook to trigger an update to the component whenever the response object changes.
So I recommend you to call initializeCodeEditor and make your app wait until response object change I used useEffect here
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
})
//skip following if block it's just for understanding
if (!response.error) {
console.log(response.document) // will obviously be null here as at first it is set null
const id = response?.document?.id; // will obviously be undefined
navigate(`/solution/${id}`, { state: true })
}
}
useEffect(() => {
if (!response.error) {
setId(response?.document?.id);
console.log("From App.js useEffect: " + response?.document?.id); // getting the document id here too
}
}, [response])
//and in firestoreReducer
case "ADDED_DOCUMENT":{
console.log("from Reducer: " + action.payload.id); //getting the document id here
return { isPending: false, document: action.payload, success: true, error: null }
}
OR you can use callback also without introducing useEffect like this:
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
}, (response) => {
console.log("From App: " + response?.document?.id); //Will run as callback
if (!response.error) {
setId(response?.document?.id);
}
})
}
This way, the callback function will be called after the addDocument function has completed and the response object will have the updated document id.

Firebase Cloud function in expo project

So I have a cloud function (this is not in the react native app directory yet):
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp();
exports.deleteUser = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall((data, context) => {
const userId = context.auth.uid;
var promises = [];
// DELETE DATA
var paths = ['users/' + userId, 'messages/' + userId, 'chat/' + userId];
paths.forEach((path) => {
promises.push(
recursiveDelete(path).then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE FILES
const bucket = admin.storage().bucket();
var image_paths = ["avatar/" + userId, "avatar2/" + userId, "avatar3/" + userId];
image_paths.forEach((path) => {
promises.push(
bucket.file(path).delete().then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE USER
promises.push(
admin.auth().deleteUser(userId)
.then( () => {
console.log('Successfully deleted user');
return true;
})
.catch((error) => {
console.log('Error deleting user:', error);
})
);
return Promise.all(promises).then(() => {
return true;
}).catch(er => {
console.error('...', er);
});
});
function recursiveDelete(path, context) {
return firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
})
.then(() => {
return {
path: path
}
}).catch( (error) => {
console.log('error: ', error);
return error;
});
}
// [END recursive_delete_function]
This is used for my swift app. How Can I use this for my react native app built with Expo?
I have installed the following yarn add #react-native-firebase/functions
I have my firebase.js file set up in the root directory:
import * as firebase from "firebase";
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "test",
authDomain: "test",
databaseURL: "test",
projectId: "test",
storageBucket: "test",
messagingSenderId: "test",
appId: "test"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
export default firebase;
I have a button:
<Text>Delete Account</Text>
<View>
<Button
title="Delete Account"
color="#F9578E"
accessibilityLabel="Delete Account"
/>
</View>
Which when clicked signs the user out and runs the above cloud function.
I'm not versed in react-native and in Expo, but from the #react-native-firebase/functions documentation it seems that you need to do as follows:
import functions from '#react-native-firebase/functions';
function App() {
useEffect(() => {
functions()
.httpsCallable('deleteUser')()
.then(response => {
// ....
});
}, []);
// ...
}
You are not passing any data from your app to your Callable Cloud Function, i.e. you are not using the data object in your Cloud Function, this is why you need to do functions().httpsCallable('deleteUser')().
If you would need to pass some data, the doc shows an example here, passing an object:
functions().httpsCallable('listProducts')({
page: 1,
limit: 15,
})
(This is totally in line with the Firebase JS SDK way of calling a Callable Cloud Function, this is why I answered to the question, even with a lack of knowledge on react-native and on Expo...)

How to test a recursive dispatch in Redux

I implemented my own way to handle access/refresh token. Basically when accessToken is expired, it awaits the dispatch of another action and, if it is successful, it dispatch again itself. The code below explains it better:
export const refresh = () => async (dispatch) => {
dispatch({
type: REFRESH_USER_FETCHING,
});
try {
const user = await api.refresh();
dispatch({
type: REFRESH_USER_SUCCESS,
payload: user,
});
return history.push("/");
} catch (err) {
const { code } = err;
if (code !== "ACCESS_TOKEN_EXPIRED") {
dispatch({
type: REFRESH_USER_ERROR,
payload: err,
});
const pathsToRedirect = ["/signup"];
const {
location: { pathname },
} = history;
const path = pathsToRedirect.includes(pathname) ? pathname : "/login";
return history.push(path);
}
try {
await dispatch(refreshToken());
return dispatch(refresh());
} catch (subErr) {
dispatch({
type: REFRESH_USER_ERROR,
payload: err,
});
return history.push("/login");
}
}
};
export const refreshToken = () => async (dispatch) => {
dispatch({
type: REFRESH_TOKEN_FETCHING,
});
try {
await api.refreshToken();
dispatch({
type: REFRESH_TOKEN_SUCCESS,
});
} catch (err) {
dispatch({
type: REFRESH_TOKEN_ERROR,
payload: err,
});
}
};
the issue is that I am finding it really difficult to test with Jest. In fact, I have implemented this test:
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import * as actionCreators from "./actionCreators";
import * as actions from "./actions";
import api from "../../api";
jest.mock("../../api");
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe("authentication actionCreators", () => {
it("runs refresh, both token expired, should match the whole flow", async () => {
api.refresh.mockRejectedValue({
code: "ACCESS_TOKEN_EXPIRED",
message: "jwt expired",
});
api.refreshToken.mockRejectedValue({
code: "REFRESH_TOKEN_EXPIRED",
message: "jwt expired",
});
const expectedActions = [
{ type: actions.REFRESH_USER_FETCHING },
{ type: actions.REFRESH_TOKEN_FETCHING },
{ type: actions.REFRESH_TOKEN_ERROR },
{ type: actions.REFRESH_USER_ERROR },
];
const store = mockStore({ auth: {} });
await store.dispatch(actionCreators.refresh());
expect(store.getActions()).toEqual(expectedActions);
});
});
but instead of completing, the test runs indefenitely. This issue is not happening when I am testing it manually, so I think there is something missing in Jest, so my question is: is there a way to test this recursive behaviour?
Thanks
The problem is await you use with dispatch, dispatch returns an action, not a Promise, use Promise.resolve instead.

Code coverage not working for Jest test with Axios

I am using Jest and moxios to write a test for my async function:
export function getData(id) {
return dispatch => {
return axios({
method: "get",
url: `${'url'}/id`
})
.then(response => {
dispatch(setData(response.data));
})
.catch(() => alert('Could not fetch data');
};
}
Test:
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import moxios from "moxios";
import getData from '../redux/getData';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const store = mockStore({});
describe('Test fetch data', () => {
beforeEach(function() {
moxios.install();
store.clearActions();
});
afterEach(function() {
moxios.uninstall();
});
it('should fetch data and set it', () => {
const data = [{ name: 'John', profession: 'developer'}];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
const expectedActions = [setData(data)];
return store.dispatch(getData()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
})
})
My test is passing, but when I check the code coverage report generated by Jest, it shows that the then block of getData was not covered/called. How can I fix this?
moxios.wait return Promise your test function return before running except functions.
you need to use done callback in your test function
it('should fetch data and set it', (done) => {
const data = [{
name: 'John',
profession: 'developer'
}];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: data
});
const expectedActions = [setData(data)];
store.dispatch(getData()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
done();
});
});
});

Testing Async Redux Action Jest

I'm having trouble getting the correct output from an async redux action. I am using Jest, redux-mock-adapter, and thunk as the tools.
According to redux's documentation on testing async thunks (https://redux.js.org/docs/recipes/WritingTests.html#async-action-creators), my tests should be returning an array of two actions. However, my test is only returning the first action, and not the second one that should return on a successful fetch. I think I'm just missing something small here, but it has been bothersome to say the least.
Redux Action
export const getRemoveFileMetrics = cacheKey => dispatch => {
dispatch({ type: IS_FETCHING_DELETE_METRICS });
return axios
.get("GetRemoveFileMetrics", { params: { cacheKey } })
.then(response => dispatch({ type: GET_REMOVE_FILE_METRICS, payload: response.data }))
.catch(err => err);
};
Test
it("getRemoveFileMetrics() should dispatch GET_REMOVE_FILE_METRICS on successful fetch", () => {
const store = mockStore({});
const cacheKey = "abc123doremi";
const removeFileMetrics = {
cacheKey,
duplicateFileCount: 3,
uniqueFileCount: 12,
};
const expectedActions = [
{
type: MOA.IS_FETCHING_DELETE_METRICS,
},
{
type: MOA.GET_REMOVE_FILE_METRICS,
payload: removeFileMetrics,
}
];
mockRequest.onGet(`/GetRemoveFileMetrics?cacheKey=${cacheKey}`).reply(200, removeFileMetrics);
return store.dispatch(MOA.getRemoveFileMetrics(cacheKey)).then(() => {
const returnedActions = store.getActions();
expect(returnedActions).toEqual(expectedActions);
});
});
The Output
Expected value to equal:
[{ "type": "IS_FETCHING_DELETE_METRICS" }, { "payload": { "cacheKey": "abc123doremi", "duplicateFileCount": 3, "uniqueFileCount": 12 }, "type": "GET_REMOVE_FILE_METRICS" }]
Received:
[{ "type": "IS_FETCHING_DELETE_METRICS" }]
I am using jest-fetch-mock and no axios. The following is working for me with the actions. You could refactor to async await as first step. For me it only worked that way.
I am now trying to figure out how to test the side effect (showErrorAlert(jsonResponse);). If I mock out the showErrorAlert implementation at the top of the test file (commented out in my example) then I get the same problem just like you. Actions that uses fetch won't get triggered for some reason.
export const submitTeammateInvitation = (data) => {
const config = {
//.....
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest());
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess());
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// jest.mock('../../../../_helpers/alerts', ()=> ({ showAlertError: jest.fn() }));
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
......
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
{
type: 'SUBMIT_TEAMMATE_INVITATION_REQUEST',
},
{
type: 'SUBMIT_TEAMMATE_INVITATION_FAILURE',
payload: { error }
}
];
return store.dispatch(submitTeammateInvitation(data))
.then(() => {
// expect(alerts.showAlertError).toBeCalled();
expect(store.getActions()).toEqual(expectedActions);
});
});

Categories