I want to change the implementation of the methods inside jest.mock so i can check how my app reacts to different edge cases so i did this, However typescript is not letting me mock firebase.auth().currentUser method ... i am showing my code and error below
app.js
import firebase from 'firebase/app'
import 'firebase/auth'
import './Init'
const App = {
getLoggedInUser: () => {
const currentUser = firebase.auth().currentUser
if (currentUser) {
return {
email: firebase.auth().currentUser.email,
userId: firebase.auth().currentUser.uid,
isEmailVerified: firebase.auth().currentUser.emailVerified
}
} else {
return undefined
}
},
isAuthenticated: () => {
return !!((App.getLoggedInUser() && App.getLoggedInUser().isEmailVerified === true))
},
}
export default App
app.spec.ts
import myAuthenticationPlugin from 'authenticationPlugin/App'
import firebase from 'firebase/app'
jest.mock('firebase/app', () => {
const userCredentialMock = {
user: {
sendEmailVerification: jest.fn()
}
}
return {
auth: jest.fn().mockReturnThis(),
currentUser: {
email: 'test',
uid: '123',
emailVerified: true
},
signInWithEmailAndPassword: jest.fn(),
createUserWithEmailAndPassword: jest.fn(() => userCredentialMock),
sendPasswordResetEmail: jest.fn(),
signOut: jest.fn(),
onAuthStateChanged: jest.fn(),
initializeApp: jest.fn()
}
})
describe('Test for isAuthenticated ()', () => {
afterEach(() => {
jest.resetAllMocks()
})
it('The API should return a boolean value telling us, If the user is authenticated to access the resources or not', () => {
expect(myAuthenticationPlugin.isAuthenticated()).toBe(true)
})
firebase.auth().currentUser = jest.fn(() => {
return {
email: 'test',
uid: '123',
emailVerified: false
}
})
it('Check false', () => {
expect(myAuthenticationPlugin.isAuthenticated()).toBe(false)
})
})
Error i get
FAIL tests/unit/App.spec.ts
● Test suite failed to run
TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
tests/unit/App.spec.ts:44:5 - error TS2740: Type 'Mock<{ email: string; uid: string; emailVerified: boolean; }, []>' is missing the following properties from type 'User': delete, emailVerified, getIdTokenResult, getIdToken, and 31 more.
44 firebase.auth().currentUser = jest.fn(() => {
~~~~~~~~~~~~~~~~~~~~~~~~~~~
I am confused now as to how to proceed with testing different edge cases for my App is there someway around this ?
You need to mock firebase.auth method and its return value as the currentUser.
E.g.
app.ts:
import firebase from 'firebase/app';
const App = {
getLoggedInUser: () => {
const currentUser = firebase.auth().currentUser;
if (currentUser) {
return {
email: currentUser.email,
userId: currentUser.uid,
isEmailVerified: currentUser.emailVerified,
};
} else {
return undefined;
}
},
isAuthenticated: () => {
return !!(App.getLoggedInUser() && App.getLoggedInUser()!.isEmailVerified === true);
},
};
export default App;
app.test.ts:
import App from './app';
import firebase from 'firebase/app';
jest.mock('firebase/app', () => {
return {
auth: jest.fn(),
};
});
describe('61408137', () => {
it('should return user', () => {
(firebase.auth as jest.Mocked<any>).mockReturnValueOnce({
currentUser: { email: 'example#gmail.com', uid: 1, emailVerified: true },
});
const actual = App.getLoggedInUser();
expect(actual).toEqual({
email: 'example#gmail.com',
userId: 1,
isEmailVerified: true,
});
});
it('should return undefined', () => {
(firebase.auth as jest.Mocked<any>).mockReturnValueOnce({});
const actual = App.getLoggedInUser();
expect(actual).toBeUndefined();
});
});
unit test results with coverage report:
PASS stackoverflow/61408137/app.test.ts (9.822s)
61408137
✓ should return user (3ms)
✓ should return undefined (1ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 50 | 87.5 |
app.ts | 87.5 | 50 | 50 | 87.5 | 17
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 11.683s
source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/61408137
Related
I have a axiosInstance.js axios instance:
import axios from "axios"
import { REACT_FE_ACCESS_TOKEN } from "../../constants/constant";
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
headers: {
"content-type": "application/json"
},
responseType: "json"
});
axiosInstance.interceptors.request.use((config) => {
const token = localStorage.getItem(REACT_FE_ACCESS_TOKEN);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export { axiosInstance };
I call it from a class:
import { axiosInstance as api } from "./axiosInstance";
export default class ApiCrud {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
fetchItems() {
return api.get(`${this.getBaseUrl()}`).then(result => result.data);
}
getBaseUrl() {
return this.baseUrl;
}
}
I want to do a write test (using Create React App and Jest).
This is the axiosInstance.test.js file, that works:
import { axiosInstance } from "../../../../utils/api/base/axiosInstance";
import { REACT_FE_ACCESS_TOKEN } from "../../../../utils/constants/constant";
const token = "a1.b2.c3";
beforeEach(() => {
localStorage.clear();
});
describe('Test API Instance', () => {
it ('Test request interceptor with token', () => {
localStorage.setItem(REACT_FE_ACCESS_TOKEN, token);
expect(localStorage.getItem(REACT_FE_ACCESS_TOKEN)).toBe(token);
const result = axiosInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
expect(result.headers).toHaveProperty("Authorization");
});
it ('Test request interceptor without token', () => {
const result = axiosInstance.interceptors.request.handlers[0].fulfilled({ headers: {} });
expect(result.headers).not.toHaveProperty("Authorization");
});
});
This is the apiCrud.test.js
import ApiCrud from "../../../../utils/api/base/ApiCrud";
const mockedGet = {
email: "info#example.com",
}
jest.mock('axios', () => {
return {
create: jest.fn(() => ({
get: jest.fn(() => Promise.resolve({ data: mockedGet })),
interceptors: {
request: { use: jest.fn(), eject: jest.fn() },
response: { use: jest.fn(), eject: jest.fn() }
}
}))
}
})
describe('Test API crud', () => {
it ('Test can get base url', () => {
const apiCrud = new ApiCrud('/fake-url');
expect(apiCrud.getBaseUrl()).toBe('/fake-url');
});
it ('Test can fetch items', () => {
const apiCrud = new ApiCrud('/fake-url');
return apiCrud.fetchItems().then(data => {
expect(data).toBe(mockedGet);
})
});
});
But I get
FAIL src/__tests__/utils/api/base/apiCrud.test.js
● Test API crud › Test can fetch items
TypeError: Cannot read property 'then' of undefined
8 |
9 | fetchItems() {
> 10 | return api.get(`${this.getBaseUrl()}`).then(result => result.data);
| ^
11 | }
12 |
13 | getBaseUrl() {
at ApiCrud.fetchItems (src/utils/api/base/ApiCrud.js:10:12)
at Object.<anonymous> (src/__tests__/utils/api/base/apiCrud.test.js:29:20)
So, I think I'm getting wrong with mocking the get for axios, but... How solve?
You are testing ApiCrud class that depends on the ./axiosInstance module. It's simpler to mock direct dependency ./axiosInstance module than indirect
dependency - axios module.
ApiCrud.js:
import { axiosInstance as api } from './axiosInstance';
export default class ApiCrud {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
fetchItems() {
return api.get(`${this.getBaseUrl()}`).then((result) => result.data);
}
getBaseUrl() {
return this.baseUrl;
}
}
ApiCrud.test.js:
import ApiCrud from './ApiCrud';
import { axiosInstance } from './axiosInstance';
const mockedGet = {
email: 'info#example.com',
};
jest.mock('./axiosInstance');
describe('Test API crud', () => {
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.resetAllMocks();
});
it('Test can get base url', () => {
const apiCrud = new ApiCrud('/fake-url');
expect(apiCrud.getBaseUrl()).toBe('/fake-url');
});
it('Test can fetch items', () => {
axiosInstance.get.mockResolvedValueOnce({ data: mockedGet });
const apiCrud = new ApiCrud('/fake-url');
return apiCrud.fetchItems().then((data) => {
expect(data).toBe(mockedGet);
});
});
});
test result:
PASS examples/69531160/ApiCrud.test.js (10.676 s)
Test API crud
✓ Test can get base url (3 ms)
✓ Test can fetch items (1 ms)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 73.33 | 0 | 80 | 71.43 |
ApiCrud.js | 100 | 100 | 100 | 100 |
axiosInstance.js | 55.56 | 0 | 0 | 55.56 | 13-17
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 14.978 s
This is my first time working with jest and react. I created an application that requires a user to be authenticated to access certain resources.
I created the following function in the Login component for authentication that makes an axios call to the server-
const loginUtil = async() => {
const res = await axios
.post("http://localhost:8080/user/authenticate", {
username,
password,
});
console.log(res.data);
if (res.data.jwt) {
localStorage.setItem("user", JSON.stringify(res.data));
} else {
throw new Error();
}
return JSON.stringify(res.data);
}
return loginUtil();
};
I wrote the following two Jest test case for the component -
import * as React from 'react'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { render, fireEvent, screen } from '#testing-library/react'
import Login from '../Login'
import {jest} from '#jest/globals'
const fakeUserResponse = {jwt: 'fake_user_token',user : {
username : 'jane',
pasword : 'pass',
role : 'user',
id : 'ftrb3344grr'
}}
const server = setupServer(
rest.post('http://localhost:8080/user/authenticate', (req, res, ctx) => {
return res(ctx.json(fakeUserResponse))
}),
);
beforeAll(() => server.listen())
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
server.resetHandlers()
window.localStorage.removeItem('user')
})
afterAll(() => server.close())
test('token is saved correctly', async() => {
render(<Login />)
fireEvent.change(screen.getByPlaceholderText(/username/i), {
target: {value: 'jane'},
})
fireEvent.change(screen.getByPlaceholderText(/password/i), {
target: {value: 'pass'}
})
fireEvent.click(screen.getByTestId(/login/i))
expect(localStorage.getItem('user')).toEqual(fakeUserResponse)
})
The first test case passes but the second doesn't. I'm also getting the following error -
Error message in the console
FAIL src/components/__tests__/Login.test.js (6.919 s)
● token is saved correctly
expect(received).toEqual(expected) // deep equality
Expected: {"jwt": "fake_user_token", "user": {"id": "ftrb3344grr", "pasword": "pass", "role": "user", "username": "jane"}}
Received: null
61 | fireEvent.click(screen.getByTestId(/login/i))
62 |
> 63 | expect(localStorage.getItem('user')).toEqual(fakeUserResponse)
| ^
64 | })
65 |
at Object.<anonymous> (src/components/__tests__/Login.test.js:63:42)
● Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "{
jwt: 'fake_user_token',
user: {
username: 'jane',
pasword: 'pass',
role: 'user',
id: 'ftrb3344grr'
}
}".
16 | } else {
17 | throw new Error();
> 18 | }
| ^
19 | return JSON.stringify(res.data);
20 | }
21 |
at BufferedConsole.log (node_modules/#jest/console/build/BufferedConsole.js:197:10)
at loginUtil (src/service/authService.js:18:13)
What this tells me is the response data is correctly sent by the mock server but for some reason the data can't be logged into the console. The test stops giving expected results beyond that.
Any kind of help regarding why this is happening would be appreciated.
Thank you in advance.
You can just mock local storage, for your use. Here is an example for it.
//Storage Mock
const storageMock = () => {
let storage = {};
return {
setItem:(key, value) => {
storage[key] = value || '';
},
getItem: (key) => {
return storage[key] || null;
},
removeItem: (key) => {
delete storage[key];
},
clear: function() {
store = {};
},
getLength: () => {
return Object.keys(storage).length;
},
key: function(i) {
const keys = Object.keys(storage);
return keys[i] || null;
}
};
}
And the you do something like:
// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
I'm trying to test a Firebase cloud function named myCloudFn in my functions/send.js module. My tests are in functions/test/send.spec.js:
// send.js
const admin = require('firebase-admin');
async function myCloudFn (email) {
const authUser = await admin.auth().getUserByEmail(email);
return authUser;
}
module.exports = { myCloudFn };
// send.spec.js
const send = require('../send.js');
jest.mock('firebase-admin', () => ({
auth: () => ({
getUserByEmail: jest.fn()
.mockResolvedValueOnce({ uid: 'foo-bar' })
.mockResolvedValueOnce(null),
}),
}));
describe('send.js', () => {
it('returns authUser from myCloudFn()', async () => {
const email = 'foo#bar.com';
const responseOptions = [{ uid: 'foo-bar' }, null];
const responsePromises = responseOptions.map(() => send.myCloudFn(email));
const responses = await Promise.all(responsePromises);
expect(responses[0]).toStrictEqual(responseOptions[0]);
expect(responses[1]).toStrictEqual(responseOptions[1]);
});
});
The test passes on the first assertion, but fails on the second. The test returns the same { uid: 'foo-bar' } object both times, but I want the test response value to be null the second time. What am I missing?
A new getUserByEmail spy is created on each auth call, it isn't called more than once.
Instead, it should be:
const mockGetUserByEmail = jest.fn();
jest.mock('firebase-admin', () => ({
auth: () => ({
getUserByEmail: mockGetUserByEmail
})
}));
...
mockGetUserByEmail
.mockResolvedValueOnce({ uid: 'foo-bar' })
.mockResolvedValueOnce(null);
const responsePromises = responseOptions.map(() => send.myCloudFn(email));
...
You can mock the resolved value once of the getUserByEmail method for each test case.
E.g.
send.js:
const admin = require('firebase-admin');
async function myCloudFn(email) {
const authUser = await admin.auth().getUserByEmail(email);
return authUser;
}
module.exports = { myCloudFn };
send.test.js:
const { myCloudFn } = require('./send');
const admin = require('firebase-admin');
jest.mock(
'firebase-admin',
() => {
const mAdmin = {
auth: jest.fn().mockReturnThis(),
getUserByEmail: jest.fn(),
};
return mAdmin;
},
{ virtual: true },
);
describe('send.js', () => {
afterAll(() => {
jest.resetAllMocks();
});
it('should returns authUser from myCloudFn()', async () => {
admin.auth().getUserByEmail.mockResolvedValueOnce({ uid: 'foo-bar' });
const actual = await myCloudFn();
expect(actual).toEqual({ uid: 'foo-bar' });
});
it('should return null', async () => {
admin.auth().getUserByEmail.mockResolvedValueOnce(null);
const actual = await myCloudFn();
expect(actual).toBeNull();
});
});
unit test result:
PASS src/stackoverflow/64575307/send.test.js (14.349s)
send.js
✓ should returns authUser from myCloudFn() (11ms)
✓ should return null (3ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
send.js | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 17.321s
I have a middleware which intercept any actions with a type that include: API_REQUEST. An action with API_REQUEST is created by the apiRequest() action creator. When my middleware intercept an action it makes a request using Axios, if the request succeeded it dispatch the action created by apiSuccess(). If Axios throw after the request, the middleware is going to dispatch the action created with apiError().
Middleware:
const apiMiddleware: Middleware = ({ dispatch }) => next => async (action): Promise<void> => {
next(action);
if (action.type.includes(API_REQUEST)) {
const body = action.payload;
const { url, method, feature } = action.meta;
try {
const response = await axios({ method, url, data: body });
dispatch(apiSuccess({ response, feature }));
} catch (error) {
console.error(error);
dispatch(apiError({ error, feature }));
}
}
};
This is how my api middleware works.
Now I'm wondering how can I test that using Jest. Maybe I can mock Axios so it makes a fake request in the middleware, but how?
Here's the current test file I have:
describe('api middleware', () => {
const feature = 'test_feat';
it('calls next', () => {
const { invoke, next } = create(apiMiddleware);
const action = { type: 'TEST' };
invoke(action);
expect(next).toHaveBeenCalledWith(action);
});
it('dispatch api success on request success', () => {
const { invoke, next, store } = create(apiMiddleware);
const action = actions.apiRequest({ body: null, method: 'GET', url: '', feature });
const data = { test: 'test data' };
jest.mock('axios');
invoke(action);
expect(next).toHaveBeenCalledWith(action);
expect(store.dispatch).toHaveBeenCalledWith(actions.apiSuccess({
response: axios.mockResolvedValue({ data }),
feature,
}));
});
});
create() is just a function that I've taken from this part of the doc. It permit me to mock dispatch, getState and next.
Obviously this doesn't worked but I'm sure there's a way.
Here is the unit test solution:
api.middleware.ts:
import { Middleware } from 'redux';
import axios from 'axios';
import { API_REQUEST } from './actionTypes';
import { apiSuccess, apiError } from './actionCreator';
export const apiMiddleware: Middleware = ({ dispatch }) => (next) => async (action): Promise<void> => {
next(action);
if (action.type.includes(API_REQUEST)) {
const body = action.payload;
const { url, method, feature } = action.meta;
try {
const response = await axios({ method, url, data: body });
dispatch(apiSuccess({ response, feature }));
} catch (error) {
console.error(error);
dispatch(apiError({ error, feature }));
}
}
};
actionTypes.ts:
export const API_REQUEST = 'API_REQUEST';
export const API_REQUEST_SUCCESS = 'API_REQUEST_SUCCESS';
export const API_REQUEST_FAILURE = 'API_REQUEST_FAILURE';
actionCreator.ts:
import { API_REQUEST_SUCCESS, API_REQUEST_FAILURE } from './actionTypes';
export function apiSuccess(data) {
return {
type: API_REQUEST_SUCCESS,
...data,
};
}
export function apiError(data) {
return {
type: API_REQUEST_FAILURE,
...data,
};
}
api.middleware.test.ts:
import { apiMiddleware } from './api.middleware';
import axios from 'axios';
import { MiddlewareAPI } from 'redux';
import { API_REQUEST, API_REQUEST_SUCCESS, API_REQUEST_FAILURE } from './actionTypes';
jest.mock('axios', () => jest.fn());
describe('59754838', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('#apiMiddleware', () => {
describe('Unit test', () => {
it('should dispatch api success action', async () => {
const store: MiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn() };
const next = jest.fn();
const action = {
type: API_REQUEST,
payload: {},
meta: { url: 'http://localhost', method: 'get', feature: 'feature' },
};
const mResponse = { name: 'user name' };
(axios as jest.Mocked<any>).mockResolvedValueOnce(mResponse);
await apiMiddleware(store)(next)(action);
expect(next).toBeCalledWith(action);
expect(axios).toBeCalledWith({ method: action.meta.method, url: action.meta.url, data: action.payload });
expect(store.dispatch).toBeCalledWith({
type: API_REQUEST_SUCCESS,
response: mResponse,
feature: action.meta.feature,
});
});
it('should dispatch api error action', async () => {
const store: MiddlewareAPI = { dispatch: jest.fn(), getState: jest.fn() };
const next = jest.fn();
const action = {
type: API_REQUEST,
payload: {},
meta: { url: 'http://localhost', method: 'get', feature: 'feature' },
};
const mError = new Error('network error');
(axios as jest.Mocked<any>).mockRejectedValueOnce(mError);
await apiMiddleware(store)(next)(action);
expect(next).toBeCalledWith(action);
expect(axios).toBeCalledWith({ method: action.meta.method, url: action.meta.url, data: action.payload });
expect(store.dispatch).toBeCalledWith({
type: API_REQUEST_FAILURE,
error: mError,
feature: action.meta.feature,
});
});
});
});
});
Unit test results with coverage report:
PASS src/stackoverflow/59754838/api.middleware.test.ts (11.206s)
59754838
#apiMiddleware
Unit test
✓ should dispatch api success action (21ms)
✓ should dispatch api error action (23ms)
console.error src/stackoverflow/59754838/api.middleware.ts:3460
Error: network error
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:42:24
at step (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:33:23)
at Object.next (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:14:53)
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:8:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:4:12)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/59754838/api.middleware.test.ts:34:46)
at Object.asyncJestTest (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmineAsyncInstall.js:102:37)
at resolve (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:43:12)
at new Promise (<anonymous>)
at mapper (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:26:19)
at promise.then (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/queueRunner.js:73:41)
at process._tickCallback (internal/process/next_tick.js:68:7)
-------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 50 | 100 | 100 | |
actionCreator.ts | 100 | 100 | 100 | 100 | |
actionTypes.ts | 100 | 100 | 100 | 100 | |
api.middleware.ts | 100 | 50 | 100 | 100 | 9 |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 12.901s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/59754838
How to code cover then and catch function of a promise inside a function while unit testing using jest? Please see code below.
Service.js
export const userLogin = data => {
return AjaxService.post(
"http://localhost/3000/signin", data
).then(
res => {
return res.data;
},
error => {
return error.response.data;
}
);
};
AjaxService.js
export const AjaxService = {
post: (url, data, headers) => {
return axios({
method: "POST",
url: url,
headers: headers || { "content-type": "application/json" },
data: data
});
}
}
Example.js
class Login extends Component {
handleSubmit = (event) => {
if (this.props.handleSubmit) this.props.handleSubmit();
this.setState({isLoggedIn: true})
userLogin().then((res) => {
// when promise resolve
var response = res;
}, (err) => {
// when promise reject
var error = err;
})
}
render() {
return (
<form id="login-form" onSubmit={(e) => this.handleSubmit(e)} >
<input type="username" />
<input type="password" />
<button type="submit">Login</button>
</form>
)
}
}
Example.test.js
it("test login form submit ", () => {
wrapper = shallow(<Login />);
let instance = wrapper.instance(); // get class instance
instance.handleSubmit(); // will trigger component method
let actualVal = wrapper.state().isLoggedIn; // get state key value
expect(true).to.eql(actualVal);
});
After generating coverage report using --coverage in Jest
We can see that the code inside promise success and error function doesn't get covered as part of unit testing. So please help cover this. Thanks.
Here is the solution, folder structure:
.
├── ajaxService.ts
├── example.spec.tsx
├── example.tsx
└── service.ts
ajaxService.ts:
import axios from 'axios';
export const AjaxService = {
post: (url, data, headers?) => {
return axios({
method: 'POST',
url,
headers: headers || { 'content-type': 'application/json' },
data
});
}
};
service.ts:
import { AjaxService } from './ajaxService';
export const userLogin = data => {
return AjaxService.post('http://localhost/3000/signin', data).then(
res => {
return res.data;
},
error => {
return error.response.data;
}
);
};
example.tsx:
import React, { Component } from 'react';
import { userLogin } from './service';
export interface ILoginProps {
handleSubmit(): void;
}
interface ILoginState {
isLoggedIn: boolean;
}
export class Login extends Component<ILoginProps, ILoginState> {
constructor(props: ILoginProps) {
super(props);
this.state = {
isLoggedIn: false
};
}
public handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (this.props.handleSubmit) {
this.props.handleSubmit();
}
this.setState({ isLoggedIn: true });
const data = {};
return userLogin(data).then(
res => {
console.log(res);
},
err => {
console.error(err);
}
);
}
public render() {
return (
<form id="login-form" onSubmit={e => this.handleSubmit(e)}>
<input type="username" />
<input type="password" />
<button type="submit">Login</button>
</form>
);
}
}
example.spec.tsx:
import React from 'react';
import { shallow } from 'enzyme';
import { Login, ILoginProps } from './example';
import * as service from './service';
describe('Login', () => {
afterEach(() => {
jest.restoreAllMocks();
jest.resetAllMocks();
});
const mockedProps: ILoginProps = { handleSubmit: jest.fn() };
const mockedFormEvent = { preventDefault: jest.fn() };
const mockedUserLoginResponse = 'mocked data';
const mockedUserLoginError = new Error('database error');
it('test login form submit - 1', done => {
const userLoginSpy = jest.spyOn(service, 'userLogin').mockResolvedValueOnce(mockedUserLoginResponse);
const logSpy = jest.spyOn(console, 'log');
const wrapper = shallow(<Login {...mockedProps}></Login>);
wrapper.find('form').simulate('submit', mockedFormEvent);
setImmediate(() => {
expect(mockedFormEvent.preventDefault).toBeCalledTimes(1);
expect(mockedProps.handleSubmit).toBeCalledTimes(1);
expect(wrapper.state('isLoggedIn')).toBeTruthy();
expect(userLoginSpy).toBeCalledWith({});
expect(logSpy).toBeCalledWith(mockedUserLoginResponse);
done();
});
});
it('test login form submit - 2', async () => {
const userLoginSpy = jest.spyOn(service, 'userLogin').mockResolvedValueOnce(mockedUserLoginResponse);
const logSpy = jest.spyOn(console, 'log');
const wrapper = shallow(<Login {...mockedProps}></Login>);
await (wrapper.instance() as any).handleSubmit(mockedFormEvent);
expect(mockedFormEvent.preventDefault).toBeCalledTimes(1);
expect(mockedProps.handleSubmit).toBeCalledTimes(1);
expect(wrapper.state('isLoggedIn')).toBeTruthy();
expect(userLoginSpy).toBeCalledWith({});
expect(logSpy).toBeCalledWith(mockedUserLoginResponse);
});
it('test login error - 1', done => {
const userLoginSpy = jest.spyOn(service, 'userLogin').mockRejectedValueOnce(mockedUserLoginError);
const errorLogSpy = jest.spyOn(console, 'error');
const wrapper = shallow(<Login {...mockedProps}></Login>);
wrapper.find('form').simulate('submit', mockedFormEvent);
setImmediate(() => {
expect(mockedFormEvent.preventDefault).toBeCalledTimes(1);
expect(mockedProps.handleSubmit).toBeCalledTimes(1);
expect(wrapper.state('isLoggedIn')).toBeTruthy();
expect(userLoginSpy).toBeCalledWith({});
expect(errorLogSpy).toBeCalledWith(mockedUserLoginError);
done();
});
});
});
Unit test result with coverage report:
PASS src/stackoverflow/58110463/example.spec.tsx
Login
✓ test login form submit - 1 (16ms)
✓ test login form submit - 2 (3ms)
✓ test login error - 1 (10ms)
console.log node_modules/jest-mock/build/index.js:860
mocked data
console.log node_modules/jest-mock/build/index.js:860
mocked data
console.error node_modules/jest-mock/build/index.js:860
Error: database error
at Suite.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/58110463/example.spec.tsx:14:32)
at addSpecsToSuite (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmine/Env.js:496:51)
at Env.describe (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmine/Env.js:466:11)
at describe (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/jasmine/jasmineLight.js:81:18)
at Object.<anonymous> (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/src/stackoverflow/58110463/example.spec.tsx:6:1)
at Runtime._execModule (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runtime/build/index.js:888:13)
at Runtime._loadModule (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runtime/build/index.js:577:12)
at Runtime.requireModule (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runtime/build/index.js:433:10)
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:201:13
at Generator.next (<anonymous>)
at asyncGeneratorStep (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:27:24)
at _next (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:47:9)
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:52:7
at new Promise (<anonymous>)
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:44:12
at _jasmine (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:206:19)
at jasmine2 (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-jasmine2/build/index.js:60:19)
at /Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runner/build/runTest.js:385:24
at Generator.next (<anonymous>)
at asyncGeneratorStep (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runner/build/runTest.js:161:24)
at _next (/Users/ldu020/workspace/github.com/mrdulin/jest-codelab/node_modules/jest-runner/build/runTest.js:181:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------------|----------|----------|----------|----------|-------------------|
All files | 86.21 | 50 | 63.64 | 84.62 | |
ajaxService.ts | 66.67 | 0 | 0 | 66.67 | 5 |
example.tsx | 100 | 75 | 100 | 100 | 21 |
service.ts | 40 | 100 | 0 | 40 | 4,6,9 |
----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 3.035s, estimated 6s
HTML coverage report:
source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/58110463
You just have to use the async await sintax:
it("test login form submit ", async () => {
wrapper = shallow(<Login />);
let instance = wrapper.instance(); // get class instance
await instance.handleSubmit(); // will trigger component method
let actualVal = wrapper.state().isLoggedIn; // get state key value
expect(true).to.eql(isLoggedIn);
});
Then, your test will "wait" until the promise is resolved or rejected and it will run inside the then or the catch. You can learn more about how Jest manages the asynchronous code here.
In addition, if you are going to work with a lot of promises, I recommend you to take a look to the wait-for-expect package. It could really help you to code your tests.