I'm wondering why I need to put fetch mock logic inside my test to make it work.
Here is simple example:
Component to test with fetch inside useEffect and state update after response:
// Test.jsx
import React, {useEffect, useState} from 'react'
export const Test = () => {
const [description, setDescription] = useState<string | null>(null)
const fetchData = async () => {
const response = await fetch('https://dummyendpoint/');
const parsed = await response.json();
const description = parsed.value;
setDescription(description);
}
useEffect(() => {
fetchData();
}, [])
return (
<div data-testid="description">
{description}
</div>
)
};
export default Test;
Test logic:
// Test.test.js
import React from 'react';
import {render, screen} from '#testing-library/react';
import Test from "./Test";
global.fetch = jest.fn(() => Promise.resolve({
json: () => Promise.resolve({
value: "Testing something!"
})
}));
describe("Test", () => {
it('Should have proper description after data fetch', async () => {
// need to put mock logic here to make it work
render(<Test/>);
const description = await screen.findByTestId('description');
expect(description.textContent).toBe("Testing something!");
});
})
If I keep global.fetch mock at the top of my test file, I keep getting an error:
TypeError: Cannot read property 'json' of undefined
at const parsed = await response.json();
It's really strange that it does not work as it is.
But I was able to fix it by moving the setup into beforeEach block (I assume beforeAll would also work).
It is a common pattern to backup global variable value, override it for tests and restore it back.
import React from 'react';
import { render, screen } from '#testing-library/react';
import Test from "./Test";
describe("Test", () => {
let originalFetch;
beforeEach(() => {
originalFetch = global.fetch;
global.fetch = jest.fn(() => Promise.resolve({
json: () => Promise.resolve({
value: "Testing something!"
})
}));
});
afterEach(() => {
global.fetch = originalFetch;
});
it('Should have proper description after data fetch', async () => {
// need to put mock logic here to make it work
render(<Test />);
const description = await screen.findByTestId('description');
expect(description.textContent).toBe("Testing something!");
});
});
Related
I am trying to create a unit test for my Profile component that calls the userService, and displays the user's information in a profile.
To do this I am using React-Testing-Library but am experiencing an issue when I am trying to mock the useParam() hook call executed within the Profile component.
This is necessary because the Profile() Component ordinarily passes in the user id of the user to generate a profile for through React Router dynamic link path variable.
Test:
import React from 'react'
// import API mocking utilities from Mock Service Worker
import {rest} from 'msw'
import {setupServer} from 'msw/node'
// import react-testing methods
import {render, fireEvent, waitFor, screen} from '#testing-library/react'
import routeData from 'react-router';
// add custom jest matchers from jest-dom
import '#testing-library/jest-dom';
import config from "../configuration/Configuration.json";
import {Profile} from "../pages/Profile";
const userId = 1;
const server = setupServer(
rest.get(config.URL + '/users/' + userId, (req, res, ctx) => {
return res(ctx.json({userId: 1, email: 'testEmail#outlook.com', password: null, firstName: 'Test', lastName: 'User'}))
}),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
beforeEach(() => jest.spyOn(routeData, 'useParams').mockReturnValue(1))
test('loads and displays profile', async () => {
render(<Profile />)
await waitFor(() => screen.getByRole('Profile'))
expect(screen.getByText('testEmail#outlook.com')).toBeVisible();
})
Profile Component:
export const Profile = () => {
//array of compatible users fetched for a user.
const [userProfileInformation, setUserProfileInformation] = useState([]);
const [isLoading, setLoading] = useState(true);
const { userId } = useParams();
useEffect(() => {
getUserProfileInformation().then(() => {
setLoading(false);
});
}, []);
const getUserProfileInformation = async () => {
const response = await UserService.getUserProfileInformation(userId)
.then(response => response.json())
.then(data => {
setUserProfileInformation(data);
});
}
if (isLoading) {
return (
<div id="loading">
<h2>Loading...</h2>
</div>
)
}
return (
<div>
<div className="profileCard">
<h1 name='fullName'>{getFullName(userProfileInformation.firstName, userProfileInformation.lastName)}</h1>
<h2>{userProfileInformation.email}</h2>
</div>
</div>
)
}
Any help would be appreciated as as far as I can see online this should work, thanks.
Edit - Error:
Error: Cannot spyOn on a primitive value; undefined given
at ModuleMocker.spyOn
at Object.<anonymous>
at Promise.then.completed
at new Promise (<anonymous>)
at callAsyncCircusFn
at _callCircusHook
at _runTest frontend\node_modules\jest-circus\build\run.js:148:5)
at _runTestsForDescribeBlock
at run
at runAndTransformResultsToJestFormat
I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});
I have following function.
const loadUsers= () => {
return async (dispatch) => {
dispatch(userRequest());
let response= null
try {
response= await UserService.getUser();
dispatch(userLoading());
} catch (error) {
dispatch(userError(error));
} finally {
dispatch(userSuccess(response));
}
};
};
With the following unit test I was abale to hit the "dispatch(userRequest());"
describe('user thunk', () => {
it('dispatches a userRequest', async () => {
const dispatch = jest.fn();
await loadUsers()(dispatch);
expect(dispatch).toHaveBeenCalledWith(userRequest());
});
});
However I couldn't figure out how to test lines and below response= await UserService.getUser();. Even though the function is not complex and I won't have much value for writing complex test, I need it for my pipeline to build.
Any help will be appreciated.
Thanks in advance.
UPDATE-> User Service
import axios from 'axios';
const USERS_ENDPOINT = '/user';
export const getUser= async () => {
const response = await axios.get(PRODUCTS_ENDPOINT, {});
return response.data;
};
export default getUser;
After days of research, I ended up testing the logic the following way.
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import * as reactRedux from 'react-redux';
import axios from 'axios';
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
describe('load user thunk', () => {
it('dispatches load user and error on call when API is not mocked', async () => {
const store = mockStore({});
const requestDispatch= userRequest();
const errorDispatch= userError("Mock Message");
await store.dispatch(await loadUsers());
const actionsResulted = store.getActions();
const expectedActions = [
requestDispatch,
errorDispatch,
];
expect(actionsResulted.length).toEqual(expectedActions.length);
expect(actionsResulted[0].type).toEqual(expectedActions[0].type);
expect(actionsResulted[1].type).toEqual(expectedActions[1].type);
});
it('dispatches load user and success on call when API is mocked', async () => {
const store = mockStore({});
const requestDispatch= userRequest();
const successDispatch= userSuccess("Mock Data");
jest
.spyOn(axios, 'get')
.mockResolvedValue({ status: 200, data: "Mock Data"});
await store.dispatch(await loadUsers());
const actionsResulted = store.getActions();
const expectedActions = [
requestDispatch,
successDispatch,
];
expect(actionsResulted.length).toEqual(expectedActions.length);
expect(actionsResulted[0].type).toEqual(expectedActions[0].type);
expect(actionsResulted[1].type).toEqual(expectedActions[1].type);
});
When I run this test the axios call is mocked correctly but setParticipant() never sets the value of the participant variable. I'm guessing it's because it's asynchronous. How do I "wait" for the setParticipant() call to complete before asserting in the test?
participant.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function EditParticipant(props) {
const [participant, setParticipant] = useState(null)
useEffect(() => {
axios.get(`/api/participants`).then(response => {
console.log(response.data) // prints fine
setParticipant(response.data)
});
}, []);
return (
<div>
{participant ? // always null
<p>{participant.name}</p>
: ''
}
</div>
);
}
participant-spec.tsx
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { act } from 'react-dom/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { EditParticipant } from './participant-edit';
const mock = new MockAdapter(require('axios'));
describe('<Participant/>', () => {
let container
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it('show bob', async () => {
mock.onGet("/api/participants").reply(200, {name: "bob" });
act(() => {
render(<EditParticipant />, container);
});
expect(container.textContent).toContain("Bob")
});
});
Given you are using async hooks try adding await in front of act to apply resolved promises:
it('show bob', async () => {
mock.onPost("/api/participants").reply(200, {name: "bob" });
await act(async () => {
render(<EditParticipant />, container);
});
expect(container.textContent).toContain("Bob")
});
This is covered briefly in the data fetching portion of the testings recipes documentation.
Hopefully that helps!
I am trying to test below function or in other words I am trying to write unit test cases of below function.But I am getting error _axios.default.get.mockResolvedValueOnce is not a function
import React from "react";
import axios from "axios";
export default () => {
const [state, setState] = React.useState([]);
const fetchData = async () => {
const res = await axios.get("https://5os4e.csb.app/data.json");
setState(res.data);
};
React.useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
return [state];
};
here is my code
https://codesandbox.io/s/awesome-jepsen-5os4e?file=/src/usetabData.test.js
I write unit test case like that
import useTabData from "./useTabData";
import { act, renderHook, cleanup } from "#testing-library/react-hooks";
import mockAxios from "axios";
describe("use tab data", () => {
afterEach(cleanup);
it("fetch tab data", async () => {
mockAxios.get.mockResolvedValueOnce({
data: {
name: "hello"
}
});
await act(async () => renderHook(() => useTabData()));
expect(mockAxios.get).toHaveBeenCalled();
});
});
Code sandbox doesn't support manual mocks as far as I know.
However, your __mock__ is placed in wrong directory structure. It should be a sibling of node_module.
Having said that, easiest way is to use https://github.com/ctimmerm/axios-mock-adapter
import useTabData from "./useTabData";
import { act, renderHook, cleanup } from "#testing-library/react-hooks";
import axios from "axios";
import MockAxiosAdapter from "axios-mock-adapter";
const mockAxios = new MockAxiosAdapter(axios);
afterEach(() => {
cleanup();// this is not needed . its done by testing library after each.
mockAxios.reset();
});
describe("use tab data", () => {
it("fetch tab data", async () => {
mockAxios.onGet(200, { data: { test: "123" } }); // response code and object.
const { result, waitForNextUpdate } = renderHook(() => useTabData());
const [value] = result.current;
// assert value
// assert the axios call by using history object
});
});
You can use history to assert: https://github.com/ctimmerm/axios-mock-adapter#history