I have a Custom Hook like below
const useSum = (a = 1, b = 1) => {
const [sum, setSum] = useState(a + b);
useEffect(() => {
setSum(a + b);
}, [a, b]);
return sum;
}
I am using this in my funcionat component
const MyFuncComp = () => {
const sum = useSum(1, 2);
return (
<div>{sum}</div>
);
}
In test case I have it like this
describe('Testing MyFuncComp', () => {
const myFuncComp = mount(<MyFuncComp />);
it('should have value of sum', () => {
const expected = '3';
const received = myFuncComp.find('div').text();
expect(received).toEqual(expected);
});
})
It's never executing 'useState' or 'useEffect'. Received value is always 'undefined';
I recommend you to use: #testing-library/react-hooks
import { renderHook } from '#testing-library/react-hooks';
describe('Testing MyFuncComp', () => {
it('should have value of sum', () => {
const myFuncComp = renderHook(() => useSum(1,2));
const expected = '3';
const received = myFuncComp.result.current;
expect(received).toEqual(expected);
});
})
Also I don't think you need enzyme or any lib to test your component, you can use react-dom and react-dom/test-utils
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import MyFunComp from "./MyFunComp";
let container = null;
describe("Card component", () => {
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("Should render correctly", async () => {
await act(async () => {
render(<MyFunComp />, container);
});
const div = container.querySelector("div");
expect(div).toBeTruthy();
expect(div.textContent).toBe("123");
});
});
Related
I have a tsx file which contains three react components:
import {FC} from 'react';
export const ComponentA: FC<{booleanProp: boolean}> = ({booleanProp}) => {
return (
<>
{booleanProp ? (
<ComponentB />
) : (
<ComponentC />
)}
</>
);
};
export const ComponentB: FC = () => {
return <span>ComponentB</span>;
};
export const ComponentC: FC = () => {
return <span>ComponentC</span>;
};
I want to test ComponentA and mock ComponentB and ComponentC.
This is my test file:
import {FC} from 'react';
import {createRoot, Root} from 'react-dom/client';
import {act} from 'react-dom/test-utils';
import {ComponentA} from './my-components';
jest.mock('./my-components', () => {
const ComponentBMock: FC = () => {
return <span>ComponentB Mock</span>;
};
const ComponentCMock: FC = () => {
return <span>ComponentC Mock</span>;
};
return {
...jest.requireActual('./my-components'),
ComponentB: ComponentBMock,
ComponentC: ComponentCMock,
};
});
describe('ComponentA', () => {
let container: Element | null = null;
let root: Root | null = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root?.unmount();
root = null;
});
container?.remove();
container = null;
});
it('should render "ComponentB" when booleanProp is true', () => {
act(() => {
root?.render(<ComponentA booleanProp={true}/>);
});
expect(container?.textContent).toEqual('ComponentB Mock');
});
it('should render "ComponentC" when booleanProp is false', () => {
act(() => {
root?.render(<ComponentA booleanProp={false}/>);
});
expect(container?.textContent).toEqual('ComponentC Mock');
});
});
The problem is that the mocks doesn't seem to take effect and these are the tests result:
Expected: "ComponentB Mock"
Received: "ComponentB"
Expected: "ComponentC Mock"
Received: "ComponentC"
When I debugged the jest mock callback it appears to be called twice. In the first time the requireActual returned undefined for every component, and in the second time it has the real components values.
What am I missing?
Thanks for your help!
So after playing with it and read more about mocking this solution solved my problem.
The difference is the way I import my components and used jest.spyOn to mock them:
import * as MyComponents from './my-components';
const ComponentA = MyComponents.ComponentA;
jest.spyOn(MyComponents, 'ComponentB').mockReturnValue(<span>ComponentB Mock</span>);
jest.spyOn(MyComponents, 'ComponentC').mockReturnValue(<span>ComponentC Mock</span>);
Of course that if you you need to remove the created mock you can restore the mock by calling the spy.mockRestore function.
The full test file:
import {createRoot, Root} from 'react-dom/client';
import {act} from 'react-dom/test-utils';
import * as MyComponents from './my-components';
const ComponentA = MyComponents.ComponentA;
jest.spyOn(MyComponents, 'ComponentB').mockReturnValue(<span>ComponentB Mock</span>);
jest.spyOn(MyComponents, 'ComponentC').mockReturnValue(<span>ComponentC Mock</span>);
describe('ComponentA', () => {
let container: Element | null = null;
let root: Root | null = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root?.unmount();
root = null;
});
container?.remove();
container = null;
});
it('should render "ComponentB" when booleanProp is true', () => {
act(() => {
root?.render(<ComponentA booleanProp={true}/>);
});
expect(container?.textContent).toEqual('ComponentB Mock');
});
it('should render "ComponentC" when booleanProp is false', () => {
act(() => {
root?.render(<ComponentA booleanProp={false}/>);
});
expect(container?.textContent).toEqual('ComponentC Mock');
});
});
Following are my code which includes the fetch API(getData) call with the useEffect and once get the response it will set the result into the setData using useState
I am trying to write the test case for the useEffect and useState but its failing and when I am seeing into the coverage ,I am getting the red background color with statements not covered for the useEffect block.
import { getData } from '../../api/data';
const [data, setData] = useState({});
useEffect(() => {
getData({ tableName }).then((response) => {
try {
if (response && response.result) {
const result = Array.isArray(response.result)
? response.result[0]
: response.result;
const createDate = result.createdDate;
result.name = result.firstName;
result.submittedDate = `${createDate}`;
result.attribute = Array.isArray(result.attribute)
? result.attribute
: JSON.parse(result.attribute);
setData(result);
}
} catch (error) {
const errorObj = { error: error.message || 'error' };
setData({ errorObj });
}
});
}, []);
And I tried to write the test cases as following for the above code.
import React from "react";
import {
shallowWithIntl,
loadTranslation,
} from "../../../node_modules/enzyme-react-intl/lib/enzyme-react-intl";
import ParentPage from "ParentPage";
import ChildPage from "ChildPage";
import mockResponse from "mockData";
import { shallow, mount } from "enzyme";
import { act } from "react-dom/test-utils";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
describe("ParentPage", () => {
let useEffect;
let wrapper;
const mockUseEffect = () => {
useEffect.mockImplementationOnce((f) => f());
};
beforeEach(() => {
const defaultProps = {
tableName: "tableName",
};
wrapper = shallowWithIntl(<ParentPage {...defaultProps} />);
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
});
it("Should render", () => {
expect(wrapper).toMatchSnapshot();
});
it("Compenent render", async () => {
let wrapper;
await act(async () => {
const setWidgets = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation([mockResponse, setWidgets]);
wrapper = await mount(<ChildPage data={mockResponse} />);
await act(async () => {
wrapper.update();
});
console.log(wrapper);
});
});
});
But when I tried using npm run test,And check the coverage I am still getting the statements not covered for the useEffect and useState.
What should I do to achieve the coverage as maximum as possible?
I am only starting with unit testing now and the course I am following has the following syntax for a test:
expect(app.state().gifts).toEqual([])
This is the syntax for the use of class components but that will be deprecated soon so I am using React function components instead.
How do you accomplish the same test with hooks?
Thanks
You can use react-hooks-testing-library and test your hooks as well. Basic Hooks
Example :
useDisclosure.ts
import * as React from 'react';
export const useDisclosure = (initial = false) => {
const [isOpen, setIsOpen] = React.useState(initial);
const open = React.useCallback(() => setIsOpen(true), []);
const close = React.useCallback(() => setIsOpen(false), []);
const toggle = React.useCallback(() => setIsOpen((state) => !state), []);
return { isOpen, open, close, toggle };
};
useDisclosure.test.ts
import { renderHook, act } from '#testing-library/react-hooks';
import { useDisclosure } from '../useDisclosure';
test('should open the state', () => {
const { result } = renderHook(() => useDisclosure());
expect(result.current.isOpen).toBe(false);
act(() => {
result.current.open();
});
expect(result.current.isOpen).toBe(true);
});
test('should close the state', () => {
const { result } = renderHook(() => useDisclosure());
expect(result.current.isOpen).toBe(false);
act(() => {
result.current.close();
});
expect(result.current.isOpen).toBe(false);
});
test('should toggle the state', () => {
const { result } = renderHook(() => useDisclosure());
expect(result.current.isOpen).toBe(false);
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBe(true);
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBe(false);
});
test('should define initial state', () => {
const { result } = renderHook(() => useDisclosure(true));
expect(result.current.isOpen).toBe(true);
act(() => {
result.current.toggle();
});
expect(result.current.isOpen).toBe(false);
});
I have this hook that should trigger beforeunload event when the compoenent is mounted and unmounted.
const UseHook = (fns: (e) => void) => {
const cb = useRef(fns);
useEffect(() => {
cb.current = fn;
}, [fn]);
useEffect(() => {
const onUnloadFN = (args: BeforeUnloadEvent) => cb.current?.(args);
window.addEventListener('beforeunload', onUnloadFN);
return () => {
window.removeEventListener('beforeunload', onUnloadFN);
};
}, []);
};
Now I want to test the hook using jest and enzyme:
import { mount } from 'enzyme';
import React from 'react';
const HookWrapper = () => {
useHook((e) => {
e.preventDefault();
e.returnValue = '';
});
return <div>component</div>;
};
describe('useHook', () => {
const location: Location = window.location;
delete window.location;
const mockPageReloading = jest.fn();
window.location = {
...location,
reload: mockPageReloading,
};
it('should mount', () => {
const mockedOnload = jest.fn();
window.addEventListener = jest.fn((event) => {
if (event === 'beforeunload') {
mockedOnload();
}
});
const wrapper = mount(<HookWrapper />);
expect(mockedOnload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
console.log(wrapper.debug());
});
it('should unmount', () => {
const mockedOnload = jest.fn();
window.removeEventListener = jest.fn((event) => {
if (event === 'beforeunload') {
mockedOnload();
}
});
const wrapper = mount(<HookWrapper />);
wrapper.unmount();
expect(mockedOnload).toHaveBeenCalledTimes(1);
});
});
The first test pass, but the second retrieve that the event listener wasn't call on unmount (it was called 0 times).
Who can help with this?
Basically I want to test if the event was triggered on mount and also on unmount.
PS: this hook is also used to detect when user reload the page. If somebody has other idea how to test this hook, please let me know.
Here is my custom hook:
useCustomModal.ts
export const useCustomModal = (modalType: string) => {
const setModal = () => {
// ... functionality here
};
const handleModalClose = (modalType: string) => {
// ... functionality here
setModal();
// ...
};
return {
handleModalClose,
setModal,
};
};
And here is my test:
useCustomModal.ts
import { act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', async () => {
await act(async () => {
const actions = useCustomModal('test');
const spy = jest.spyOn(actions, 'setModal');
actions.handleModalClose('test');
expect(spy).toBeCalledTimes(1);
});
});
});
Test failed :
Expected number of calls: 1
Received number of calls: 0
How to properly spyOn on custom react hooks?
You need to use renderHook in conjunction with act. Something like this:
import { renderHook, act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', () => {
const { result } = renderHook(() => useCustomModal('test'));
const spy = jest.spyOn(result.current, 'setModal');
act(() => {
result.current.handleModalClose('test');
});
expect(spy).toBeCalledTimes(1);
});
});