Testing amplify Auth with Jest + Enzyme - reactjs

I am very new to testing and I finally feel like I've got the hang of it. However, mocks are still a bit confusing. I am currently testing a signup function and the functions executes up until Auth.signUp. I'm not sure if I need to mock something in my test or if I need it to run through a different test.
async function signUp(
{ first, last, email, password }: SignupUserType,
dispatch: Dispatcher,
formContent: FormContentType,
setFormContent: SetFormContent,
) {
console.log('signing in init...');
dispatch({ type: 'INIT' });
try {
const user = await Auth.signUp({
username: email,
password,
attributes: {
given_name: first,
family_name: last,
picture: userImage,
},
});
console.log('sign up success!');
dispatch({ type: 'STOP_LOADING' });
console.log(formContent);
setFormContent(formContent);
} catch (err) {
console.log('error signing up...', err);
dispatch({ type: 'ERROR', error: err.message, doing: 'SIGNUP' });
}
}
Test
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../../aws-exports';
Amplify.configure(awsconfig);
jest.mock('aws-amplify');
it('SIGNUP: Completed form fields enable button', async () => {
...
wrapper
.find('#submitButton')
.at(0)
.simulate('click');
// thought I could do something like from https://stackoverflow.com/questions/51649891/how-to-mock-aws-library-in-jest
Auth.signUp = jest.fn().mockImplementation(
() => {
// return whatever you want to test
});
// or I tried something like from https://markpollmann.com/testing-react-applications
expect(Amplify.Auth.signUp).toHaveBeenCalled();
// kept getting errors about not receiving the call
})

I got it working!
import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../../aws-exports';
Amplify.configure(awsconfig);
Auth.signUp = jest.fn().mockImplementation(
() => {
return true;
});
it('SIGNUP: Completed form fields enable button', async () => {
...
wrapper
.find('#submitButton')
.at(0)
.simulate('click');
})

thanks for the tip!
However, in my case implementation of Auth.forgotPassword mock has to return new Promise, because in the code I use it like
Auth.forgotPassword(email)
.then(...)
.catch(...)
so my mock is:
Auth.forgotPassword = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => {
resolve({
CodeDeliveryDetails: {
AttributeName: 'email',
DeliveryMedium: 'EMAIL',
Destination: 's***#y***.ru',
},
})
})
})
Hope it helps someone who will stumble with the same issue

Another option would be to use:
jest.spyOn(Auth, "signUp").mockImplementation(() => {});
Through this you can omit jest.mock('aws-amplify'); and only need to import "Auth" and not Amplify itself

Related

getting issue in jest unit testing for login

i have created login page,which works fine, when i created unit test for that module with jest, i am getting issue, it doesn't return promiss when login api calls, here is my code for react and jest respectively, i am not getting console for console.log("after login"); can anyone please check my code and help to resolve this issue ?
validateAll(formData, rules, message).then(async () => {
dispatch(setLoading(true))
console.log("before login");
const login = await authApi.login(app, email, password)
console.log("after login");
if (login && login.error && login.error !== null) {
dispatch(setLoading(false))
ToastAlert({ msg: login.error.error || login.error.message, msgType: 'error' });
dispatch(setToken(''))
} else {
console.log("done");
dispatch(setToken(login.data._accessToken))
setSuccess(true)
dispatch(setLoading(false))
//router.replace("/");
ToastAlert({ msg: 'You have successfully logged in', msgType: 'success' });
}
}
auth
import { resolve } from "./resolve";
import * as Realm from "realm-web";
function auth() {
const register = async (app: any, data: object) => {
return await resolve(
app.emailPasswordAuth.registerUser(data)
.then((response: any) => response)
)
}
const login = async (app: any, email: string, password: string) => {
const credentials = await Realm.Credentials.emailPassword(email, password);
return await resolve(
app.logIn(credentials)
.then((response: any) => response)
)
}
const confirmUser = async (app: any, data: object) => {
return await resolve(
app.emailPasswordAuth.confirmUser(data)
.then((response: any) => response)
)
}
const logout = async (app: any) => {
return await resolve(
app.currentUser.logOut()
.then((response: any) => response)
)
}
return {
register,
login,
confirmUser,
logout
}
}
const authApi = auth();
export default authApi;
unit test
test('login with correct username and password', async () => {
const initialState = {
email: '',
password: '',
error: {
email: "",
password: ""
},
};
const mockStore = configureStore();
let store;
store = mockStore(initialState);
const { getByText,getByLabelText } = render(
<Provider store={store}>
<Authenticate />
</Provider>
);
// fill out the form
fireEvent.change(screen.getByLabelText('Email'), {
target: {value: '***#gmail.com'},
})
fireEvent.change(screen.getByLabelText(/Parola/i), {
target: {value: '****#123'},
})
const loginAwait = screen.getByText(/Authentificare/i);
await fireEvent.click(loginAwait)
// wait for the error message
const alert = await screen.findByRole('alert')
expect(alert).toHaveTextContent(/You have successfully logged in/i)
})
As mentioned in the comments you can't make real requests in jest. You have 2 big options to solve your issue:
Option 1.1: As was mentioned in the comment (and shown in the link) you can mock your request library (in your case is not axios, is Realm.Credentials.emailPassword, but you probably also have to mock the app.logIn part) :
Just replace Realm, for example, you should add something like this to your unit test:
...
const Realm = require("realm-web");
jest.mock("realm-web")
...
test('login with correct username and password', async () => {
Realm.Credentials.emailPassword.mockResolvedValue({token: "test-token"})
})
WARNING: As mentioned above this by itself most likely won't fix your problem since you have to also mock app.logIn (however assuming app.logIn is just calling under the hood Realm.App.logIn you might be able to mock that too by adding:
Realm.App.logIn.mockResolvedValue({user: {...})
(Realm.App might need to be Realm.App())
If Realm.Credentials.emailPassword throws an error you might need to define them first when defining jest.mock("realm-web"). So something like :
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: jest.fn()
}
},
Credentials: {
emailPassword: jest.fn()
}
}))
or you can just mock the library at the beginning using something like:
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: (token:string) => ({user: {...}})
}
},
Credentials: {
emailPassword: (email: string, password:string) => ({token: "test-token"})
}
}))
)
If this is not the case you need to figure how to mock that as well (is kind of hard to properly fix your issue without a working example). But assuming you are doing something like app = new Realm.App(...) you might want to check how to mock new Function() with Jest here. If you get to this, you will most likely need a hybrid solution (to mock both new Realm.App and Realm.Credentials.emailPassword)
You could also try to mock the entire module at once, at the beginning of the test file using something like:
jest.mock("realm-web", () => ({
App: (config) => {
return {
logIn: (token:string) => ({user: {...}})
}
},
Credentials: {
emailPassword: (email: string, password:string) => ({token: "test-token"})
}
}))
OBS: adjusments might be required. This is just an example.
Also please be aware that this will create a mock for all the tests following the execution of this code.
Option 1.2:
You could also use a similar strategy to mock your authApi (you have some examples on how to mock a default export here). Just make sure you mock the the login function from it (and if there is any other function from the api -> like confirmUser used on the same test mock that as well). This option would be easier to implement but it's really up to what you want your test to cover. Also a hybrid might be mocking the app part together with Realm.Credentials.emailPassword.
Option 2: You might find an existing solution. Here are some interesting links:
github Realm mock
react native example from mogoDB page -> this won't work copy-pasted, but might serve as inspiration.
Another maybe somehow related question would be How to fake Realm Results for tests. It is not answering your question but might also help a little.

Mock axios create in test file. React, typescript

I've seen some similar posts about mocking axios but I have spend some hours and I didn't manage to solve my problem and make my test work. I've tried solutions that I have found but they didn't work.
I'm writing small app using React, Typescript, react-query, axios. I write tests with React Testing Library, Jest, Mock Service Worker.
To test delete element functionality I wanted just to mock axios delete function and check if it was called with correct parameter.
Here is the PROBLEM:
I'm using axios instance:
//api.ts
const axiosInstance = axios.create({
baseURL: url,
timeout: 1000,
headers: {
Authorization: `Bearer ${process.env.REACT_APP_AIRTABLE_API_KEY}`,
},
//api.ts
export const deleteRecipe = async (
recipeId: string
): Promise<ApiDeleteRecipeReturnValue> => {
try {
const res = await axiosInstance.delete(`/recipes/${recipeId}`);
return res.data;
} catch (err) {
throw new Error(err.message);
}
};
});
//RecipeItem.test.tsx
import axios, { AxiosInstance } from 'axios';
jest.mock('axios', () => {
const mockAxios = jest.createMockFromModule<AxiosInstance>('axios');
return {
...jest.requireActual('axios'),
create: jest.fn(() => mockAxios),
delete: jest.fn(),
};
});
test('delete card after clicking delete button ', async () => {
jest
.spyOn(axios, 'delete')
.mockImplementation(
jest.fn(() =>
Promise.resolve({ data: { deleted: 'true', id: `${recipeData.id}` } })
)
);
render(
<WrappedRecipeItem recipe={recipeData.fields} recipeId={recipeData.id} />
);
const deleteBtn = screen.getByRole('button', { name: /delete/i });
user.click(deleteBtn);
await waitFor(() => {
expect(axios.delete).toBeCalledWith(getUrl(`/recipes/${recipeData.id}`));
});
});
In test I get error "Error: Cannot read property 'data' of undefined"
However if I would not use axios instance and have code like below, the test would work.
//api.ts
const res = await axios.delete(`/recipes/${recipeId}`);
I'm pretty lost and stuck. I've tried a lot of things and some answers on similar problem that I've found on stackoverflow, but they didn't work for me. Anybody can help?
I don't want to mock axios module in mocks, only in specific test file.
I don't have also experience in Typescript and testing. This project I'm writing is to learn.
I found some workaround and at least it's working. I moved axiosInstance declaration to a separate module and then I mocked this module and delete function.
//RecipeItem.test.tsx
jest.mock('axiosInstance', () => ({
delete: jest.fn(),
}));
test('delete card after clicking delete button and user confirmation', async () => {
jest
.spyOn(axiosInstance, 'delete')
.mockImplementation(
jest.fn(() =>
Promise.resolve({ data: { deleted: 'true', id: `${recipeData.id}` } })
)
);
render(
<WrappedRecipeItem recipe={recipeData.fields} recipeId={recipeData.id} />
);
const deleteBtn = screen.getByRole('button', { name: /delete/i });
user.click(deleteBtn);
await waitFor(() => {
expect(axiosInstance.delete).toBeCalledWith(`/recipes/${recipeData.id}`);
});
});
If you have a better solution I would like to see it.

How to test firebase functions in saga?

I'm trying to test the function below:
export function* signInWithEmail({ payload: { email, password } }) {
try {
const { user } = yield auth.signInWithEmailAndPassword(email, password);
yield call(getSnapShotFromUserAuth, user);
} catch (error) {
yield put(signInFailure(error));
}
}
I have looked into redux-saga-test-plan as well as other saga testing libraries but can't seem to figure out how to test firebase function if it's not called with saga effects.
I need a way to mock firebase function at the same time being able to test that this line is being called yield call(getSnapShotFromUserAuth, user);
Had to use jest.spyOn for the firebase function and then verify the next step. Answer below:
it("should call getSnapShotFromUserAuth", () => {
let userData = {
user: {
id: 1,
name: "jon",
},
};
let { user } = userData;
jest
.spyOn(auth, "signInWithEmailAndPassword")
.mockImplementation(() => userData);
let saga = testSaga(signInWithEmail, { payload });
saga
.next()
.next({ user })
.call(getSnapShotFromUserAuth, user)
.next()
.isDone();
});

How can I mock multiple get axios request in Jest unit test for an async action that calls a few more async actions?

I'm using axios mock for testing redux actions in my app. I stuck with the problem of testing an async action function that sends POST request and dispatches a few more async actions that send their own GET requests. How can I manage that using mocks?
Actions:
export function recordStepCompleted(step, isNewLevel = true) {
return function (dispatch) {
// setUserTaskComplete() action sends `POST` request
dispatch(setUserTaskComplete(step))
.then(() => isNewLevel && dispatch(rewardsActions.getUserLevels()));
dispatch({
type: types.RECORD_STEP_COMPLETED,
payload: { step }
});
};
}
export function getUserLevels() {
return function (dispatch, getState) {
dispatch(getAchievements()) // it sends GET request
.then(achievements => {
dispatch({ type: types.GET_LEVELS.REQUEST });
return api.getLevels() // it also sends GET request
.then(res => {
//......
dispatch({
type: types.GET_LEVELS.SUCCESS,
payload: { levels: { ...res.data, ... } }
});
})
.catch(error => /*....*/);
});
};
}
So, in total, I need to mock three different requests to test the recordStepCompleted action but I don't know how. Please, help me with that. Thanks in advance!
You can extend AxiosStatic Interface to create your own and use jest.fn() to
pass your responses:
import axios, { AxiosStatic } from "axios";
jest.mock("axios");
interface AxiosMock extends AxiosStatic {
mockResolvedValue: (promise: Promise<Partial<AxiosResponse>>) => void;
mockRejectedValue: (error: Partial<AxiosError>) => void;
mockResolvedValueOnce: (promise: Promise<Partial<AxiosResponse>>) => this;
}
const mockAxios = axios as AxiosMock;
const myMock = jest.fn();
myMock
.mockReturnValueOnce({ "Your DATA 1" })
.mockReturnValueOnce({ "Your DATA 2" })
.mockReturnValueOnce({ "Your DATA 3" });
mockAxios
.mockResolvedValueOnce(Promise.resolve(myMock()))
.mockResolvedValueOnce(Promise.resolve(myMock()))
.mockResolvedValue(Promise.resolve(myMock()));

React Hooks - How to test changes on global providers

I'm trying to test the following scenario:
A user with an expired token tries to access a resource he is not authorized
The resources returns a 401 error
The application updates a global state "isExpiredSession" to true
For this, I have 2 providers:
The authentication provider, with the global authentication state
The one responsible to fetch the resource
There are custom hooks for both, exposing shared logic of these components, i.e: fetchResource/expireSesssion
When the resource fetched returns a 401 status, it sets the isExpiredSession value in the authentication provider, through the sharing of a setState method.
AuthenticationContext.js
import React, { createContext, useState } from 'react';
const AuthenticationContext = createContext([{}, () => {}]);
const initialState = {
userInfo: null,
errorMessage: null,
isExpiredSession: false,
};
const AuthenticationProvider = ({ authStateTest, children }) => {
const [authState, setAuthState] = useState(initialState);
return (
<AuthenticationContext.Provider value={[authStateTest || authState, setAuthState]}>
{ children }
</AuthenticationContext.Provider>);
};
export { AuthenticationContext, AuthenticationProvider, initialState };
useAuthentication.js
import { AuthenticationContext, initialState } from './AuthenticationContext';
const useAuthentication = () => {
const [authState, setAuthState] = useContext(AuthenticationContext);
...
const expireSession = () => {
setAuthState({
...authState,
isExpiredSession: true,
});
};
...
return { expireSession };
}
ResourceContext.js is similar to the authentication, exposing a Provider
And the useResource.js has something like this:
const useResource = () => {
const [resourceState, setResourceState] = useContext(ResourceContext);
const [authState, setAuthState] = useContext(AuthenticationContext);
const { expireSession } = useAuthentication();
const getResource = () => {
const { values } = resourceState;
const { userInfo } = authState;
return MyService.fetchResource(userInfo.token)
.then((result) => {
if (result.ok) {
result.json()
.then((json) => {
setResourceState({
...resourceState,
values: json,
});
})
.catch((error) => {
setErrorMessage(`Error decoding response: ${error.message}`);
});
} else {
const errorMessage = result.status === 401 ?
'Your session is expired, please login again' :
'Error retrieving earnings';
setErrorMessage(errorMessage);
expireSession();
}
})
.catch((error) => {
setErrorMessage(error.message);
});
};
...
Then, on my tests, using react-hooks-testing-library I do the following:
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
// Here is the issue, how to test the global value of the Authentication context? the line below, of course, doesn't work
expect(result.current.isExpiredSession).toBeTruthy();
});
I have tried a few solutions:
Rendering the useAuthentication on the tests as well, however, the changes made by the Resource doesn't seem to reflect on it.
Exposing the isExpiredSession variable through the Resource hook, i.e:
return {
...
isExpiredSession: authState.isExpiredSession,
...
};
I was expecting that by then this line would work:
expect(result.current.isExpiredSession).toBeTruthy();
But still not working and the value is still false
Any idea how can I implement a solution for this problem?
Author of react-hooks-testing-library here.
It's a bit hard without being able to run the code, but I think your issue might be the multiple state updates not batching correctly as they are not wrapped in an act call. The ability to act on async calls is in an alpha release of react (v16.9.0-alpha.0) and we have an issue tracking it as well.
So there may be 2 ways to solve it:
Update to the alpha version and a move the waitForNextUpdate into the act callback
npm install react#16.9.0-alpha.0
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
await act(async () => {
result.current.getResource();
await waitForNextUpdate();
});
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Add in a second waitForNextUpdate call
it.only('Should fail to get resource with invalid session', async () => {
const wrapper = ({ children }) => (
<AuthenticationProvider authStateTest={{ userInfo: { token: 'FOOBAR' }, isExpiredSession: false }}>
<ResourceProvider>{children}</ResourceProvider>
</AuthenticationProvider>
);
const { result, waitForNextUpdate } = renderHook(() => useResource(), { wrapper });
fetch.mockResponse(JSON.stringify({}), { status: 401 });
act(() => result.current.getResource());
// await setErrorMessage to happen
await waitForNextUpdate();
// await setAuthState to happen
await waitForNextUpdate();
expect(result.current.errorMessage).toEqual('Your session is expired, please login again');
expect(result.current.isExpiredSession).toBeTruthy();
});
Your appetite for using alpha versions will likely dictate which option you go for, but, option 1 is the more "future proof". Option 2 may stop working one day once the alpha version hits a stable release.

Resources