I'm trying to write a unit test for a redux async action creator using jest.
asyncActions.js:
const startSignInRequest = () => ({
type: START_SIGNIN_REQUEST
});
// action creator to dispatch the success of sign In
export const signInSucceded = user => ({
type: SIGNIN_USER_SUCCEEDED,
user
});
// action creator to dispatch the failure of the signIn request
export const signInFailed = error => ({
type: SIGNIN_USER_FAILED,
error
});
const signInUser = user => dispatch => {
dispatch(startSignInRequest);
return signInApi(user).then(
response => {
const { username, token } = response.data;
dispatch(signInSucceded(username));
localStorage.setItem("token", token);
history.push("/homepage");
},
error => {
let errorMessage = "Internal Server Error";
if (error.response) {
errorMessage = error.response.data;
}
dispatch(signInFailed(errorMessage));
dispatch(errorAlert(errorMessage));
}
);
};
signInApi.js:
import axios from "axios";
import { url } from "../../env/config";
const signInApi = async user => {
const fetchedUser = await axios.post(`${url}/signIn`, {
email: user.email,
password: user.password
});
return fetchedUser;
};
In the Writing tests of redux's official documentation, they use fetch-mock library. However, I think that this library call the real Api.
I tried to mock the axios api using jest mocks.
/__mocks/signInApi.js:
const users = [
{
login: 'user 1',
password: 'password'
}
];
export default function signInApi(user) {
return new Promise((resolve, reject) => {
const userFound = users.find(u => u.login === user.login);
process.nextTick(() =>
userFound
? resolve(userFound)
// eslint-disable-next-line prefer-promise-reject-errors
: reject({
error: 'Invalid user credentials',
}),
);
});
}
__tests/asyncActions.js:
jest.mock('../axiosApis/signInApi');
import * as actions from '../actions/asyncActions';
describe('Async action creators', async () => {
it('Should create SIGN_IN_USER_SUCCEEDED when signIn user has been done', () => {
const user = {
login: 'user 1',
password: 'password'
}
await expect(actions.signInUser(user)).resolves.toEqual({
user
})
})
});
The test failed and I got:
expect(received).resolves.toEqual()
Matcher error: received value must be a promise
Received has type: function
Received has value: [Function anonymous]
How can I mock this async action creator only with jest?
Looks like you need to update your mock to resolve to an object like this:
export default function signInApi(user) {
return new Promise((resolve, reject) => {
const userFound = users.find(u => u.login === user.login);
process.nextTick(() =>
userFound
? resolve({ // <= resolve to an object
data: {
username: 'the username',
token: 'the token'
}
})
// eslint-disable-next-line prefer-promise-reject-errors
: reject({
error: 'Invalid user credentials',
}),
);
});
}
...then what you are really testing is that actions.signInUser returns a function which can be called with a user...
...which then returns another function which can be called with a dispatch which dispatches the proper actions:
jest.mock('./signInApi');
import * as actions from './asyncActions';
describe('Async action creators', () => {
it('Should create SIGN_IN_USER_SUCCEEDED when signIn user has been done', async () => {
const user = {
login: 'user 1',
password: 'password'
};
const dispatch = jest.fn();
await actions.signInUser(user)(dispatch); // <= call the function on a user, then call the resulting function on a dispatch
expect(dispatch).toHaveBeenCalledTimes(2); // Success!
expect(dispatch).toHaveBeenNthCalledWith(1, { type: START_SIGNIN_REQUEST }); // Success!
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SIGNIN_USER_SUCCEEDED, user: 'the username' }); // Success!
})
});
EDIT: I have to edit my answer as the first one pointed to a wrong direction.
So from my understanding you want to mock the Action + Return value. In your case I would just immediately return the result of your mock function. As you're not mocking axios.post you don't need to wrap everything inside a promise and return that. You're not mocking only the HTTP call but the whole action.
const users = [
{
login: 'user 1',
password: 'password'
}
];
export default function signInApi(user) {
const userFound = users.find(u => u.login === user.login);
return (userFound ? userFound : {
error: 'Invalid user'
});
}
Related
I'm trying to create a simple test with nestjs, and I'm getting this error
Test functions cannot both take a 'done' callback and return something. Either use a 'done' callback, or return a promise.
Returned value: Promise {}
The unit test is so simple, but I get an error when I use done();
it('throws an error if a user signs up with an email that is in use', async (done) => {
fakeUsersService.find = () => Promise.resolve([{ id: 1, email: 'a', password: '1' } as User]);
try {
await service.signup('asdf#asdf.com', 'asdf');
} catch (err) {
done();
}
});
You are combining Async/Await and Done.
Either use asnyc/await, or done.
it('throws an error if user signs up with email that is in use', async () => {
try {
await service();
expect(...);
} catch (err) {
}
});
or use the done format
it('throws an error if user signs up with email that is in use', (done) => {
...
service()
.then( ...) {}
.catch( ...) {}
}
done();
});
for the last version from jest, you can't use `async/await , promise and done together.
the solution is
it("throws an error if user sings up with email that is in use", async () => {
fakeUsersService.find = () =>
Promise.resolve([{ id: 1, email: "a", password: "1" } as User]);
await expect(service.signup("asdf#asdf.com", "asdf")).rejects.toThrow(
BadRequestException
);
});
change BadRequestException according to your listening exception
Before v27, jest use jest-jasmine2 by default.
For version 27, jest uses jest-circus which doesn’t support done callback.
So you need to change the default testRunner.
Override with react-app-rewired worked for me
// config-overrides.js
module.exports.jest = (config) => {
config.testRunner = 'jest-jasmine2';
return config;
};
For the last version from jest, you can't use `async/await , promise and done together (Test functions cannot both take a 'done' callback and return something. Either use a 'done' callback, or return a promise.).
the solution is
user.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
AfterInsert,
AfterRemove,
AfterUpdate,
} from 'typeorm';
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
email: string;
#Column()
password: string;
#AfterInsert()
logInsert() {
console.log('Inserted User with id', this.id);
}
#AfterUpdate()
logUpdate() {
console.log('Updated User with id', this.id);
}
#AfterRemove()
logRemove() {
console.log('Removed User with id', this.id);
}
}
auth.service.spec.ts
it('throws an error if user signs up with email that is in use', async () => {
fakeUsersService.find = () =>
Promise.resolve([{ id: 1, email: 'typescript#nestjs.jestjs', password: '1' } as User]);
expect(async () => {
const email = 'asdf#asdf.com';
const password = 'asdf';
await service.signup(email, password);
}).rejects.toThrow(BadRequestException);
});
Also, if you want to use both you can downgrade your current version of jest to : 26.6.3.
Worked fine for me, I'm using async + done
it('throws an error if a user signs up with an email that is in use', async () => {
await service.signup('asdf#asdf.com', 'asdf');
try {
await service.signup('asdf#asdf.com', 'asdf');
} catch (e) {
expect(e.toString()).toMatch('email in use');
}
});
in order for it to work, you can do the following:
it('throws an error if a user signs up with an email that is in use', async () => {
fakeUsersService.find = () =>
Promise.resolve([
{ id: 1, email: 'test#test.com', password: 'somePassword' } as User,
]);
expect(async () => {
await service.signup('test#test.com', 'somePassword')
}).rejects.toThrow(BadRequestException)
});
You can use this hack for some cases =)
it('should make an api request', (done) => {
const asyncCall = async () => {
await callbackWithApiInside();
setTimeout(() => {
expect(api).toHaveBeenNthCalledWith(1, payload);
done();
}, 1000);
};
asyncCall();
});
how to test this Api and get 100% score of testing coverage?
const login = async (email, password) => {
axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then((response) => response);
};
Your function is relatively simple : one path, no branching logic, one external call.
All your function do is calling an endpoint through axios.post.
login.js
export const login = async (email, password) => {
/*
* Notice that I added the 'await', else 'async' is useless.
* Else you can directly return the axios.post method.
*/
await axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then((response) => response); // This line is also useless for the moment
};
login.spec.js
import { login } from './login';
// Mock axios, else you will really request the endpoint
jest.mock('axios');
import axios from 'axios';
describe('Login tests', () => {
describe('login function', () => {
const email = 'test#test.com';
const password = 'password';
beforeEach(() => {
/*
* Not necessary for the moment, but will be useful
* to test successful & error response
*/
axios.post.mockResolvedValue({});
});
it('should call endpoint with given email & password', async () => {
await login(email, password);
expect(axios.post).toBeCalledWith(
'https://conduit.productionready.io/api/users/login',
{ user: { email, password } },
);
});
});
});
Notice that you could greatly improve your login function by returning something and handling error with an Authentication Error. Your tests would be more significant :
errors.js
export class DefaultError extends Error {
static STATUS_CODE = 500; // You can change it, it depends how you use it
name = 'DefaultError';
constructor() {
super('Default error, add what you want');
}
}
export class AuthenticationError extends Error {
static STATUS_CODE = 401;
name = 'AuthenticationError';
constructor() {
super('Wrong credentials');
}
}
login.js
import { AuthenticationError, DefaultError } from './errors';
export const login = async (email, password) =>
axios
.post('https://conduit.productionready.io/api/users/login', {
user: {
email,
password,
},
})
.then(response => response.data)
.catch(error => {
// Handles the error how you want it
if (error.status === AuthenticationError.STATUS_CODE) {
throw new AuthenticationError();
}
throw new DefaultError();
});
login.spec.js
import { login } from './login';
import { AuthenticationError, DefaultError } from './errors';
// Mock axios, else you will really request the endpoint
jest.mock('axios');
import axios from 'axios';
describe('Login tests', () => {
describe('login function', () => {
const email = 'test#test.com';
const password = 'password';
describe('with success', () => {
const data = { something: {} };
beforeEach(() => {
axios.post.mockResolvedValue({ data });
});
it('should call endpoint with given email & password', async () => {
await login(email, password);
expect(axios.post).toBeCalledWith(
'https://conduit.productionready.io/api/users/login',
{ user: { email, password } },
);
});
it('should return response data', async () => {
const response = await login(email, password);
expect(response).toStrictEqual(data);
});
});
describe('with error', () => {
describe('status 401', () => {
beforeEach(() => {
axios.post.mockRejectedValue({ status: 401 });
});
it('should throw AuthenticationError', async () => {
await expect(login(email, password)).rejects.toThrow(AuthenticationError);
});
});
describe('other status', () => {
beforeEach(() => {
axios.post.mockRejectedValue({});
});
it('should throw DefaultError', async () => {
await expect(login(email, password)).rejects.toThrow(DefaultError);
});
});
});
});
});
We could go further but I think you got the point. Btw, you don't need to split the tests as I did, I just enjoy being able to group the describe by the mocks needed and making little & readable tests.
I am using jumbo 4.1.1 template. I get the response, but this error occurs.
My code is below:
authAction.js:
...
export const userSignIn = (user) => {
// const {email, password} = user;
console.log(user)
axios.post(base_url +'login', user)
.then(response=> {
console.log(response.data)
return {
type: SIGNIN_USER,
payload: user
};
})
};
But when I run the project, I got the error TypeError: Cannot read property 'type' of undefined
I believe that is because you have not dispatched the action properly.
Are you using thunk?
If you are using Redux and making an async call, you should return dispatch.
const userSignup = (signupData) => {
return async (dispatch) => {
try {
let res = await axios.post(`${url}/user/`, { user: signupData });
dispatch({
type: "FETCH_CURRENT_USER_SUCCESS",
payload: res.data.user,
});
return true;
} catch (error) {
console.log(
"%c CATCH in userSignup ",
"background: red; color: #bada55",
error
);
}
};
};
Notice, that I am returning a function with dispatch as an argument in the second line. and then using that dispatch (4th line) that dispatches the action's type and payload.
Also I'm returning something from the function userSignUp, finally, so the invoker of the function userSignUp gets something in response.
If you don't understand anything above, please do let me know.
You could benefit from using async + await here.
For example:
export const userSignIn = async (user) => {
try {
// const {email, password} = user;
console.log(user);
const data = await axios.post(base_url +'login', user);
console.log(data);
return {
type: SIGNIN_USER,
payload: user,
};
} catch (error) {
console.log(error);
// return an error action...?
return {
type: ERROR,
payload: error,
};
}
};
I need some help with jest test. I am getting error TypeError: Cannot read property 'then' of undefined in my test
signIn function in Action Creators makes a POST call
import {SIGNING_IN, SIGNED_IN, SIGNING_FAILED, SIGNED_OUT} from "../actionTypes";
import {makePostCall} from "../../api/apiCalls";
export function signIn(data){
return dispatch => {
dispatch({type: SIGNING_IN});
makePostCall('http://localhost:8000/api/v1/signin', data)
.then(response => {
const auth = {token: response.token, userId: response.id};
localStorage.setItem('auth', JSON.stringify(auth));
dispatch({type: SIGNED_IN, message: 'You signed in successfully.'});
})
.catch(error => {
console.log('error: ', error);
dispatch({type: SIGNING_FAILED, message: 'Email or Password incorrect. Please try again!'});
});
}
}
this is POST call function which gets called in above call
export function makePostCall(url, data){
return axios({method: 'post', url, data})
.then(response => response.data)
}
Test for signIn Method
jest.mock('../../../src/api/apiCalls');
describe('authenticationActionCreators', () => {
describe('signIn', () => {
let dispatch;
beforeEach(() => {
jest.clearAllMocks();
const authResponse = getAuthResponse();
makePostCall.mockReturnValue(Promise.resolve(authResponse));
dispatch = jest.fn();
});
test('should make post call with correct URL and data', () => {
const data = {email: 'user#user.com', password: 'password'};
return signIn(data)(dispatch).then(() => {
expect(makePostCall).toHaveBeenCalledWith('http://localhost:8000/api/v1/signin', {
email: 'user#user.com',
password: 'password'
})
})
});
});
Whenever i run test i get error on line return signIn(data)(dispatch).then(() => {
i was doing it wrong. I changed it and it works
test('should make post call with correct URL and data', () => {
const data = {email: 'user#user.com', password: 'password'};
signIn(data)(dispatch);
expect(makePostCall).toHaveBeenCalledWith('http://localhost:8000/api/v1/signin', {
email: 'user#user.com',
password: 'password'
})
});
I need to post data from a user to my database (firebase), but when I click on Touchable Opacity, my app returns this error:
Single Ocurrence
Possible Unhandled Promise rejection (id:0): TypeError: undefined is not an object (evaluating 'res.data'
My code:
import {USER_LOGGED_IN, USER_LOGGED_OUT } from './actionTypes'
import axios from 'axios'
const authBaseURL ='https://www.googleapis.com/identitytoolkit/v3/relyingparty'
const API_KEY ='my api key is here'
export const login = user => {
return {
type: USER_LOGGED_IN,
payload: user
}
}
export const logout = () => {
return {
type: USER_LOGGED_OUT
}
}
export const createUser = user => {
return dispatch => {
axios.post (`${authBaseURL}/signupNewUser?key=${API_KEY}`, {
email: user.email,
password: user.password,
returnSecureToken:true
})
.catch(err => console.log(err))
.then (res => {
if (res.data.localId) {
axios.put(`/users/${res.data.localId}.json`, {
name: user.name,
tipouser: user.tipouser,
telcelular: user.telcelular,
telfixo: user.telfixo,
facebookuser: user.facebookuser,
instagramuser: user.instagramuser
})
.catch(err => console.log(err))
.then(res => {
console.log('Usuário criado com sucesso')
})
}
})
}
}
react-native: 0.60.4
axios: 0.19.0
react-redux: 7.1.1
redux: 4.0.4"