I am trying to test rendered data on my page that is coming from an api. The api generates one of three random objects, in this case they are weather and contain the following types:
forcast: string;
max: number;
min: number;
description: string;
I'm new to testing and typescript and am wondering how I can find say the forecast on a page if it is going to be randomised each call.
here is my code thus far:
Weather.tsx
import axios from 'axios';
import { useEffect, useState } from 'react';
import { IWeather } from '../interfaces/IWeather';
const Weather = () => {
const [minTemp, setMinTemp] = useState<IWeather[]>([]);
const [maxTemp, setMaxTemp] = useState<IWeather[]>([]);
const [forcast, setForcast] = useState([]);
const fetchWeatherData = async () => {
const response = await axios.get('http://mock-api-call/weather/get-weather');
setMinTemp(response.data.result.weather.min);
setMaxTemp(response.data.result.weather.max);
setForcast(response.data.result.weather.forcast);
};
useEffect(() => {
fetchWeatherData();
}, []);
return (
<div className="container">
<p className="forcast">{forcast}</p>
<p className="temperature">Temperature {`${minTemp} to ${maxTemp}`}</p>
</div>
);
};
export default Weather;
And this is a basic version of say testing for all the forecast elements. I have tried adding || and searching for multiple strings but unfortunately is not working
Weather.test.tsx
import { render, screen } from '#testing-library/react';
import Weather from '../Weather';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('http://mock-api-call/weather/get-weather', (req, res, ctx) => {
return res(ctx.json({}));
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('Weather', () => {
test('should render forcast for day', async () => {
render(<Weather />);
const forecast = await screen.findAllByText(/sunny/i || /overcast/i || /snowing/i);
expect(forecast).toBeInTheDocument();
});
});
I am also receiving this TypeError
TypeError: Cannot read properties of undefined (reading 'weather')
10 | const fetchWeatherData = async () => {
11 | const response = await axios.get('http://mock-api-call/weather/get-weather');
> 12 | setMinTemp(response.data.result.weather.min);
| ^
13 | setMaxTemp(response.data.result.weather.max);
14 | setForcast(response.data.result.weather.forcast);
15 | };
For clarity the data is rendering onto the page.
Related
I am trying to test a functional component which is below :
export const MyFunctionalComponent=()=>{
const {id, otherid} = useSelector((state) => state.user);
const accounts = useSelector((state) => state.Selection.accounts);
const dispatch = useDispatch();
useEffect(() => {
if (!accounts) {
dispatch(getDetails(id, otherid));
}
}, []);
const handleOnClick = (Id) => {
dispatch(saveId(Id));
};
if (!accounts) {
return <CircularLoader />;
}
if (!accounts.length) {
return (
<AccessUnavailablePanel
/>
);
}
if (accounts.length === 1) {
return <Redirect to={`${accounts[0].id}`} />;
}
return (
<MyList
accounts={accounts}
handleOnClick={handleOnClick}
/>
);
};
i am new to JEST , how can i correctly mock useSelector, useDispatch and useEffect ?
i am trying to test as follows :
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
useDispatch: jest.fn()
}));
describe('<MyFunctionalComponent/>', () => {
const useEffect=jest.spyOn(React, "useEffect").mockImplementation(() => {})
const useSelectorMock = reactRedux.useSelector;
const useDispatchMock = reactRedux.useDispatch;
const user = {
id: '32sdfsdfr-fjdfdk33-4434sdfs',
otherid: '73587hdfjg-dfghd94-jgdj'
};
const store = mockStore({
user:user,
Selection: {
Id: '',
accounts: null
}
});
let wrapper;
const setup = () => {
const {result} = renderHook(() => MyFunctionalComponent(), {
wrapper: ({children}) => (
<Provider store={store}>{children}</Provider>
)
});
return result;
};
describe('Rendering accounts available', () => {
beforeEach(() => {
useDispatchMock.mockImplementation(() => () => {});
useSelectorMock.mockImplementation((selector) =>
selector(mockStore)
);
});
afterEach(() => {
jest.clearAllMocks();
useSelectorMock.mockClear();
useDispatchMock.mockClear();
});
it('should render the accounts if more than 1 account available', () => {
useSelectorMock.mockReturnValue(user);
const mockdata = {
accounts: [
{
id: '637ghgh',
},
{
id: '10a190abd',
}
]
};
const getDetails = jest.spyOn(
SelectionActions,
'getDetails'
);
useSelectorMock.mockReturnValue(mocdata);
useDispatchMock.mockReturnValue(
getDetails(user.id, user.otherid)
);
wrapper = setup();
store.dispatch(getDetails(user.id, user.otherid))
sleep(500);
const actions=store.getActions();
console.debug(actions[0])
expect(getDetails).toHaveBeenCalledWith(user.id, user.otherid);
expect(actions[0].type).toEqual('GET_DETAILS')
expect(wrapper.current).toMatchSnapshot();
});
});
But my snapshot seems to return AccessUnavialable, and the payload is undefined. How can I modify my tests to correctly return the mocked data as payload and get the correct snapshot?
It can be tested by mocking libraries such as react-redux, but it is not recommended. It pays too much attention to implementation details. A better way is to use the mock store to provide only mock data instead of mock implementation details. Use the original, unmocked useSelector and useEffect hooks, which are closer to the real function of the code, rather than the implementation details of the mock.
In addition, creating mock objects and their implementation are also very cumbersome steps. You should also pay attention to cleaning up mock objects to avoid being used by other test cases, resulting in test failures.
Use redux-mock-store to create a mock store with the mocked state.
Each test case provides different mock data to drive component rendering, and finally, assert whether the component rendering is correct.
For simplicity, just to demonstrate this test method, I replaced your component with a simple component, the principle is the same.
E.g.
index.tsx:
import { useSelector, useDispatch } from 'react-redux';
import { useEffect } from 'react';
import React from 'react';
const getDetails = (id, otherId) => ({ type: 'GET_DETAILS' });
export const MyFunctionalComponent = () => {
const { id, otherid } = useSelector((state: any) => state.user);
const accounts = useSelector((state: any) => state.Selection.accounts);
const dispatch = useDispatch();
useEffect(() => {
if (!accounts) {
dispatch(getDetails(id, otherid));
}
}, []);
if (!accounts) {
return <div>CircularLoader</div>;
}
if (!accounts.length) {
return <div>AccessUnavailablePanel</div>;
}
if (accounts.length === 1) {
return <div>Redirect</div>;
}
return <div>MyList</div>;
};
index.test.tsx:
import { MyFunctionalComponent } from './';
import createMockStore from 'redux-mock-store';
import { screen, render } from '#testing-library/react';
import { Provider } from 'react-redux';
import React from 'react';
const mockStore = createMockStore([]);
describe('69013562', () => {
test('should render AccessUnavailablePanel', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('AccessUnavailablePanel')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
test('should render CircularLoader and dispatch get details action', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: {},
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('CircularLoader')).toBeTruthy();
expect(store.getActions()).toEqual([{ type: 'GET_DETAILS' }]);
});
test('should render Redirect', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [1] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('Redirect')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
test('should render MyList', () => {
const state = {
user: { id: '1', otherid: '2' },
Selection: { accounts: [1, 2, 3] },
};
const store = mockStore(state);
render(<MyFunctionalComponent />, {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
expect(screen.getAllByText('MyList')).toBeTruthy();
expect(store.getActions()).toEqual([]);
});
});
test result:
PASS examples/69013562/index.test.tsx (10.158 s)
69013562
✓ should render AccessUnavailablePanel (28 ms)
✓ should render CircularLoader and dispatch get details action (3 ms)
✓ should render Redirect (2 ms)
✓ should render MyList (2 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 11.014 s
There is no single answer to the test strategy, adjust according to the specific situation.
I have an issue with mocking / testing interactions with socket.io-client in my react application. I have tried methods such as mocking, libraries such as socket.io-mock and articles such as medium article. However, it did not came close to even emitting an event for my web application to consume.
socket_config.ts
import io from 'socket.io-client';
import config from '../utils/config';
const ws = io(config.socket_endpoint);
export default ws;
ScanPaperDocument.tsx
import React, { useState, useEffect } from 'react';
import ws from '../../socket';
const ScanPaperDocument: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
const [scanning, setScanning] = useState<boolean>(true);
useEffect(() => {
ws.on('text_socket', (data: any) => {
// Process the data, do some state change
// Assuming that data is alright
setLoading(false);
setScanning(true);
});
ws.on('image_socket', (data: any) => {
// Process the data, do some state change
});
}, []);
return (
<>
{loading && scanning && <p>Scanning. Please wait.</p>}
{!loading && scanning && <p>Here is your data.</p>}
{/* And many other loading & scanning combinations */}
</>
);
};
export default ScanPaperDocument;
Here is a trashy mock implementation that I attempted to do
ScanPaperDocument.test.tsx
import React from 'react';
import ScanPaperDocument from './ScanPaperDocument';
import { act, render, waitFor } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
jest.mock('socket.io-client', () => {
return jest.fn().mockImplementation(() => {
return {
on: jest.fn(),
emit: jest.fn(),
};
});
});
describe(() => {
it('is able to consume the incoming websocket event', () => {
const { getByText } = render(<ScanPaperDocument />);
// Not sure how do I 'emit' an event from the 'server' for my frontend to consume
expect(getByText(/here is your data/i)).toBeTruthy();
})
})
As for the medium article that I have attempted, I received the following error by simply calling serverSocket.emit('text_socket', {data: ''})
TypeError: Cannot read property 'forEach' of undefined
2 |
3 | const emit = (event, ...args) => {
> 4 | EVENTS[event].forEach((func) => func(...args));
| ^
5 | };
6 |
7 | const socket = {
at Object.emit (src/socket/mock-socket.io-client.js:4:17)
I am trying to test a simple component.
In my component, I am calling the fetchPdf function with useEffect.
Inside fetchPdf function I am making axios request and if the call was successful I am setting the response (which is the pdf) in the state.
My component:
import React, { useState, useEffect } from "react";
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
// Here I am make an API call - on success I am
// set the pdf from the response into state
// axios.get('url/endpoint')
// .then((res) => {
// if (res.status === 200) {
// setPdf(res.data);
// }
// }).catch((e) => console.log(e.message));
setPdf("Mocked PDF");
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
My test:
import React from "react";
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import App from "./App";
Enzyme.configure({ adapter: new Adapter() });
describe("<App />", () => {
let wrapper;
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation((init) => [init, setState]);
beforeEach(() => {
wrapper = Enzyme.shallow(<App />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("calling designPDF", () => {
it("set designPDF into the state", () => {
// Not sure how can I test the fetchPDF function as been called
});
});
});
Codesendbox example
useEffect does not support shallow rendering yet. So you should use Full Rendering API (mount(...)).
Besides, you should mock axios.get method and its resolved value. Call the whenStable function to ensure that the promise returned by the axios.get method is resolved or rejected
E.g.
App.jsx:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function App() {
const [pdf, setPdf] = useState();
const fetchPdf = () => {
axios
.get('url/endpoint')
.then((res) => {
if (res.status === 200) {
setPdf(res.data);
}
})
.catch((e) => console.log(e.message));
};
useEffect(() => {
fetchPdf();
}, []);
return (
<div className="App">
<h1>My pdf from state: {pdf}</h1>
</div>
);
}
App.test.jsx:
import App from './App';
import axios from 'axios';
import { mount } from 'enzyme';
import React from 'react';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65310275', () => {
it('should get pdf', async () => {
const getSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: 'Mocked PDF', status: 200 });
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: Mocked PDF');
expect(getSpy).toBeCalledWith('url/endpoint');
getSpy.mockRestore();
});
it('should handle error', async () => {
const mErr = new Error('timeout');
const getSpy = jest.spyOn(axios, 'get').mockRejectedValueOnce(mErr);
const logSpy = jest.spyOn(console, 'log');
const wrapper = mount(<App></App>);
await whenStable();
expect(wrapper.find('h1').text()).toEqual('My pdf from state: ');
expect(getSpy).toBeCalledWith('url/endpoint');
expect(logSpy).toBeCalledWith('timeout');
getSpy.mockRestore();
});
});
unit test result:
PASS examples/65310275/App.test.jsx
65310275
✓ should get pdf (40 ms)
✓ should handle error (13 ms)
console.log
timeout
at CustomConsole.<anonymous> (node_modules/jest-environment-enzyme/node_modules/jest-mock/build/index.js:866:25)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
App.jsx | 100 | 50 | 100 | 100 | 11
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 6.095 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65310275
To start I'm conditionally rendering a component that's reliant on the useState hook being set as a result of useEffect hook. Here's a code sample:
function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
My current point of contention is that I'm unable to call setResponse or setFail and update the state of fail or response. I believe that I need to use mount as opposed to shallow rendering? Also, I understand that testing philosophies would argue against conducting a test in this fashion. However, I am seeking an a solution that enables updating the state. Any advice would be greatly appreciated.
You can mock fetch API call to and its resolved value. Then, you can assert what exactly the component renders. We should use whenStable function to make sure the mocked API calls are completed.
Component.jsx:
import React, { useState, useEffect } from 'react';
export function Component() {
const [response, setResponse] = useState();
const [fail, setFail] = useState();
/**
* #function getModels - fetch data to populate the models
* */
const fetchStuff = async () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((data) => {
const storage = [data];
setResponse(storage);
setFail(false);
})
.catch((err) => {
setResponse(err);
setFail(true);
});
};
useEffect(() => {
fetchStuff();
}, []);
if (fail === true) {
return (
<p>
ERROR:
{fail}
</p>
);
}
if (fail === false) {
return (
<p>
Success:
{response}
</p>
);
}
return <p>Loading Screen</p>;
}
Component.test.jsx:
import React from 'react';
import { Component } from './Component';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
const whenStable = async () => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
};
describe('65243384', () => {
let fetch;
beforeEach(() => {
fetch = global.fetch;
});
afterEach(() => {
global.fetch = fetch;
});
it('should success', async () => {
global.fetch = jest.fn().mockResolvedValueOnce('mocked success data');
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('Success:mocked success data');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
it('should fail', async () => {
const mErr = new Error('network');
global.fetch = jest.fn().mockRejectedValueOnce(mErr);
const wrapper = mount(<Component></Component>);
expect(wrapper.find('p').text()).toBe('Loading Screen');
await whenStable();
expect(wrapper.find('p').text()).toBe('ERROR:');
expect(global.fetch).toBeCalledWith('https://jsonplaceholder.typicode.com/todos/1');
});
});
unit test result:
PASS examples/65243384/Component.test.jsx
65243384
✓ should success (44 ms)
✓ should fail (5 ms)
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
Component.jsx | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.121 s
source code: https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/65243384
I want to test api call and data returned which should be displayed inside my functional component. I created List component which performs api call. I would like the returned data to be displayed in the component and I use the useState hook for this. Component looks like this:
const List: FC<{}> = () => {
const [data, setData] = useState<number>();
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
}
func();
}, [])
return (
<div>
<div id="test">{data}</div>
</div>
)
}
I wrote one test in which I mocked the fetch method. I check if the fetch method has been called and it actually happens. Unfortunately, I don't know how I could test the value returned from response. When I try console.log I just get null and I'd like to get 'example text'. My guess is that I have to wait for this value returned from Promise. Unfortunately, despite trying with methods act and wait, I don't know how to achieve it. Here is my test:
it('test', async () => {
let component;
const fakeResponse = 'example text';
const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
await wait( async () => {
component = render(<List />);
})
const value: Element = component.container.querySelector('#test');
console.log(value.textContent);
expect(mockedFetch).toHaveBeenCalledTimes(1);
})
I would be really thankful for any suggestions.
Second Attempt
Also tried using data-testid="test" and waitForElement, but still receiving null value.
updated component deltas:
const List: FC<{}> = () => {
- const [data, setData] = useState<number>();
+ const [data, setData] = useState<string>('test');
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
}
func();
}, [])
return (
<div>
- <div id="test">{data}</div>
+ <div data-testid="test" id="test">{data}</div>
</div>
)
}
and updated test:
it('test', async () => {
const fakeResponse = 'example text';
const mockFetch = Promise.resolve({json: () => Promise.resolve(fakeResponse)});
const mockedFetch = jest.spyOn(window, 'fetch').mockImplementationOnce(() => mockFetch as any )
const { getByTestId } = render(<List />);
expect(getByTestId("test")).toHaveTextContent("test");
const resolvedValue = await waitForElement(() => getByTestId('test'));
expect(resolvedValue).toHaveTextContent("example text");
expect(mockedFetch).toHaveBeenCalledTimes(1);
})
Here is a working unit testing example:
index.tsx:
import React, { useState, FC } from 'react';
export const List: FC<{}> = () => {
const [data, setData] = useState<number>();
const getData = (): Promise<any> => {
return fetch('https://jsonplaceholder.typicode.com/todos/1');
};
React.useEffect(() => {
const func = async () => {
const data = await getData();
const value = await data.json();
setData(value.title);
};
func();
}, []);
return (
<div>
<div data-testid="test">{data}</div>
</div>
);
};
index.test.tsx:
import { List } from './';
import React from 'react';
import '#testing-library/jest-dom/extend-expect';
import { render, waitForElement } from '#testing-library/react';
describe('59892259', () => {
let originFetch;
beforeEach(() => {
originFetch = (global as any).fetch;
});
afterEach(() => {
(global as any).fetch = originFetch;
});
it('should pass', async () => {
const fakeResponse = { title: 'example text' };
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes as any);
(global as any).fetch = mockedFetch;
const { getByTestId } = render(<List></List>);
const div = await waitForElement(() => getByTestId('test'));
expect(div).toHaveTextContent('example text');
expect(mockedFetch).toBeCalledTimes(1);
expect(mRes.json).toBeCalledTimes(1);
});
});
unit test result:
PASS src/stackoverflow/59892259/index.test.tsx (9.816s)
59892259
✓ should pass (63ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.tsx | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.73s, estimated 13s
waitForElement is not available from '#testing-library/react' anymore. Docs
Another approach is:
import { act, render } from '#testing-library/react';
it('is a test definition', async () => { // notice the async
await act(async () => { // this is kind of ugly, but it works.
render(<TheComponent />
})
// this section will run after the effects within TheComponent were triggered
})
What worked for me was a combination of both answers
it("should fetch data", async ()=>{
const fakeResponse = {title : "Test"}
const mRes = { json: jest.fn().mockResolvedValueOnce(fakeResponse) };
const mockedFetch = jest.fn().mockResolvedValueOnce(mRes);
global.fetch = mockedFetch;
render(<Component/>);
await act(async ()=>{
await waitFor(() => expect(mockedFetch).toHaveBeenCalledTimes(1))
})
})
NB: the code above is in javascript, but I don't think there is much difference between js and ts