How to test functional component with async callback inside useEffect using snapshots - javascript

I'm trying to write unit tests on my component, it looks like this.
export const myComponent = ({text, list, getData = transport.getData}) => {
const [rows, setRows] = React.useState([]);
React.useEffect(() => {
const fetchData = async () => {
const rows = await getData(list);
setRows(rows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return (
// some JSX
);
};
The problem is that component fetches data via async function, so it will be called after the component check if rows is empty and return null.
if (rows.length === 0) {
return null;
}
I mocked getData so it should return some values. But still, I couldn't understand how I should cover this component with unit testing. I suppose it should be a snapshot, perhaps it is not right.
My test:
import React from 'react';
import {myComponent} from '../components/myComponent';
import renderer from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mock = {
text: 'text',
list: [],
getData: () =>
Promise.resolve([
{
// ...
},
]),
};
const component = renderer.create(<myComponent text={mock.text}
list={mock.list} getData={mock.getData}/>);
let popup = component.toJSON();
expect(popup).toMatchSnapshot();
});
});

Here is the unit test solution:
index.tsx:
import React from 'react';
const transport = {
async getData(list) {
return [{ id: 1 }];
}
};
export const MyComponent = ({ text, list, getData = transport.getData }) => {
const [rows, setRows] = React.useState<any[]>([]);
React.useEffect(() => {
console.count('useEffect');
const fetchData = async () => {
console.count('fetchData');
const newRows = await getData(list);
setRows(newRows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return <div>rows count: {rows.length}</div>;
};
index.spec.tsx:
import React from 'react';
import { MyComponent } from './';
import renderer, { act } from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mProps = {
text: 'text',
list: [],
getData: jest.fn().mockResolvedValueOnce([{ id: 1 }, { id: 2 }, { id: 3 }])
};
let component;
await act(async () => {
component = renderer.create(<MyComponent {...mProps}></MyComponent>);
});
expect(component.toJSON()).toMatchSnapshot();
});
});
Unit test result:
PASS src/stackoverflow/57778786/index.spec.tsx
myComponent
✓ should renders correctly (29ms)
console.count src/stackoverflow/57778786/index.tsx:13
useEffect: 1
console.count src/stackoverflow/57778786/index.tsx:15
fetchData: 1
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 3.557s, estimated 8s
index.spec.tsx.snap:
// Jest Snapshot v1
exports[`myComponent should renders correctly 1`] = `
<div>
rows count:
3
</div>
`;
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57778786

Related

How to test if button was clicked?

I'm working with React Testing Library. The problem I have is that fireEvent doesn't trigger, and the error throws, is following:
Expected number of calls: >= 1
Received number of calls: 0
For this particular test do I need to work with #testing-library/react-hooks? Do I need to render my Product component wrapped on context provider where the function is imported from?
Product.tsx component
import React, { FunctionComponent } from "react";
import { useProducts } from "./ContextWrapper";
import { Button } from "#mui/material";
import { articleProps } from "./types";
const Product: FunctionComponent<{ article: articleProps }> = ({
article,
}) => {
const { handleAddToCart } = useProducts(); // this is from global context
return (
<Button
aria-label="AddToCart"
onClick={() => handleAddToCart(article)}
>
Add to cart
</Button>
);
};
export default Product;
Product.test.tsx
import React from "react";
import { fireEvent, render, screen } from "#testing-library/react";
import Product from "./Product";
import { act, renderHook } from "#testing-library/react-hooks";
const article = {
name: "samsung",
variantName: "phone",
categories: [],
};
const renderProduct = () => {
return render(
<ContextProvider>
<Product article={article} />
</ContextProvider>
);
};
//first test just tests if button exists with provided content and works fine
test("renders product", () => {
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
expect(AddToCartbutton).toHaveTextContent("Add to cart");
});
// this test throws the error described above
test("test addToCart button", () => {
renderProduct();
const onAddToCart = jest.fn();
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalled();
});
ContextWrapper.tsx
import React, {createContext, useContext, ReactNode, useState} from "react";
import { prodProps } from "../types";
type ProductContextProps = {
productData: prodProps;
handleAddToCart: (clickedItem: productPropsWithAmount) => void;
};
const ProductContextDefaultValues: ProductContextProps = {
productData: null as any,
handleAddToCart:null as any;
};
type Props = {
children: ReactNode;
};
const ProductContext = createContext<ProductContextProps>(
ProductContextDefaultValues
);
export function useProducts() {
return useContext(ProductContext);
}
const ContextWrapper = ({ children }: Props) => {
const { data } = useGraphQlData();
const [productData, setProductData] = useState<Category>(data);
const [cartItems, setCartItems] = useState<prodProps[]>([]);
useEffect(() => {
if (data) {
setProductData(data);
}
}, [data]);
const handleAddToCart = (clickedItem: prodProps) => {
setCartItems((prev: prodProps[]) => {
return [...prev, { ...clickedItem }];
});
};
return (
<ProductContext.Provider
value={{
productData,
handleAddToCart,
}}
>
{children}
</ProductContext.Provider>
);
};
export default ContextWrapper;
I advise you to create Button Component and test it separately like this:
const onClickButton = jest.fn();
await render(<Button onClick={onClickButton} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
await fireEvent.click(AddToCartbutton);
expect(onClickButton).toHaveBeenCalled();
One way to accomplish this is to mock ContextWrapper, as your test is specifically referring to Product component.
So, you could do something like this into your test:
import * as ContextWrapper from '--- PATH TO ContextWrapper ---';
test('test addToCart button', () => {
/// mockFunction
const onAddToCart = jest.fn();
jest.spyOn(ContextWrapper, 'useProducts').mockImplementationOnce(() => ({
productData: {
example_prodProp1: 'initialValue1',
example_prodProp2: 'initialValue2',
},
// Set mockFunction to handleAddToCart of useProducts
handleAddToCart: onAddToCart,
}));
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole('button', { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalledWith({
prodProp1: 'samsung',
prodProp2: 'phone',
});
});
In this line jest.spyOn(ContextWrapper, 'useProducts').mockImplementation we are mocking useProducts return value and setting the handleAddToCart function to your mockFunction and that's how you can check if it has been called.
* This test is strictly focused on Product component and you just want to garantee that the component calls the handleAddToCart function from ContextWrapper.
For test how handleAddToCart should work, you can create a specific test for the ContextWrapper.

custom hook testing: when testing, code that causes React state updates should be wrapped into act(...):

Following this tutorial https://www.richardkotze.com/coding/mocking-react-hooks-unit-testing-jest, but getting this error even though the test passes, why is this error occurring and is there something missing from the test? code copied here for convenience
PASS src/use-the-fetch.spec.js
● Console
console.error
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at TestComponent (~/Documents/projects/my-app/node_modules/#testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)
at Suspense
at ErrorBoundary (~/Documents/projects/my-app/node_modules/react-error-boundary/dist/react-error-boundary.cjs.js:59:35)
5 | async function fetchData() {
6 | const data = await getStarWars(path); // being mocked
> 7 | setResult({ loading: false, data });
| ^
8 | }
9 | useEffect(() => {
10 | fetchData();
at console.error (node_modules/#testing-library/react-hooks/lib/core/console.js:19:7)
at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
at setResult (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
at fetchData (src/use-the-fetch.js:7:5)
use-the-fetch.spec.js
import { renderHook } from "#testing-library/react-hooks";
import { useTheFetch } from "./use-the-fetch";
import { getStarWars } from "./base-fetch";
jest.mock("./base-fetch");
describe("use the fetch", () => {
it("initial data state is loading and data empty", () => {
const { result } = renderHook(() => useTheFetch("people"));
expect(result.current).toStrictEqual({ loading: true, data: null });
});
it("data is fetched and loading is complete", async () => {
const fakeSWData = { result: [{ name: "Luke Skywalker" }] };
getStarWars.mockResolvedValue(fakeSWData);
const { result, waitForNextUpdate } = renderHook(() =>
useTheFetch("people")
);
await waitForNextUpdate();
expect(getStarWars).toBeCalledWith("people");
expect(result.current).toStrictEqual({
loading: false,
data: fakeSWData,
});
});
});
use-the-fetch.js
import { useState, useEffect } from "react";
import { getStarWars } from "./base-fetch";
export function useTheFetch(path) {
const [result, setResult] = useState({ loading: true, data: null });
async function fetchData() {
const data = await getStarWars(path); // being mocked
setResult({ loading: false, data });
}
useEffect(() => {
fetchData();
}, []);
return result;
}
base-fetch.js
const BASE_URL = "https://swapi.co/api/";
export async function baseFetch(url, options = {}) {
const response = await fetch(url, options);
return await response.json();
}
export const getStarWars = async (path) => baseFetch(BASE_URL + path);

Timeout - Async callback was not invoked within the 5000ms

I created this hook:
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { loading, data } = useQuery(GET_DECIDER, { variables: { name } });
console.log('loading:', loading);
console.log('data:', data);
return { enabled: data?.deciders[0]?.decision, loading };
};
Im trying to test it with react testing library:
const getMock = (decision) => [
{
request: {
query: GET_DECIDER,
variables: { name: 'FAKE_DECIDER' },
},
result: {
data: {
deciders: [{ decision }],
},
},
},
];
const FakeComponent = () => {
const { enabled, loading } = useDecider('FAKE_DECIDER');
if (loading) return <div>loading</div>;
console.log('DEBUG-enabled:', enabled);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
// Test
import React from 'react';
import { render, screen, cleanup, act } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
describe('useDecider', () => {
afterEach(() => {
cleanup();
});
it('when no decider provided - should return false', async () => {
render(<MockedProvider mocks={getMock(false)}>
<FakeComponent />
</MockedProvider>
);
expect(screen.getByText('loading')).toBeTruthy();
act((ms) => new Promise((done) => setTimeout(done, ms)))
const result = screen.findByText('isDisabled');
expect(result).toBeInTheDocument();
});
});
I keep getting this error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

Not able to mock a function inside useEffect

I have a custom hook as below
export const useUserSearch = () => {
const [options, setOptions] = useState([]);
const [searchString, setSearchString] = useState("");
const [userSearch] = useUserSearchMutation();
useEffect(() => {
if (searchString.trim().length > 3) {
const searchParams = {
orgId: "1",
userId: "1",
searchQuery: searchString.trim(),
};
userSearch(searchParams)
.then((data) => {
setOptions(data);
})
.catch((err) => {
setOptions([]);
console.log("error", err);
});
}
}, [searchString, userSearch]);
return {
options,
setSearchString,
};
};
and I want to test this hook but am not able to mock userSearch function which is being called inside useEffect.
can anybody help?
this is my test
it('should set state and test function', async () => {
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
)
const { result } = renderHook(
() => useUserSearch(),
{ wrapper }
)
await act(async () => {
result.current.setSearchString('abc5')
})
expect(result.current.options).toEqual(expected)
})
useUserSearchMutation
import {createApi, fetchBaseQuery} from '#reduxjs/toolkit/query/react';
export const userSearchAPI = createApi({
reducerPath: 'userSearchResult',
baseQuery: fetchBaseQuery({baseUrl: process.env.REACT_APP_BASE_URL}),
tagTypes: ['Users'],
endpoints: build => ({
userSearch: build.mutation({
query: body => ({url: '/org/patient/search', method: 'POST', body}),
invalidatesTags: ['Users'],
}),
}),
});
export const {useUserSearchMutation} = userSearchAPI;
Because it's a named export you should return an object in the mock
it("should set state and test function", async () => {
jest.mock("./useUserSearchMutation", () => ({
useUserSearchMutation: () => [jest.fn().mockResolvedValue(expected)],
}));
const wrapper = ({ children }) => (
...
});
I have created a smaller example based on your code, where I am mocking a hook inside another hook.
hooks/useUserSearch.js
import { useEffect, useState } from "react";
import useUserSearchMutation from "./useUserSearchMutation.js";
const useUserSearch = () => {
const [text, setText] = useState();
const userSearch = useUserSearchMutation();
useEffect(() => {
const newText = userSearch();
setText(newText);
}, [userSearch]);
return text;
};
export default useUserSearch;
hooks/useUSerSearchMutation.js
I had to move this to its own file to be able to mock it when it was called
inside of the other hook.
const useUserSearchMutation = () => {
return () => "Im not mocked";
};
export default useUserSearchMutation;
App.test.js
import { render } from "react-dom";
import useUserSearch from "./hooks/useUserSearch";
import * as useUserSearchMutation from "./hooks/useUserSearchMutation";
import { act } from "react-dom/test-utils";
let container;
beforeEach(() => {
// set up a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
document.body.removeChild(container);
container = null;
});
function TestComponent() {
const text = useUserSearch();
return <div>{text}</div>;
}
test("should mock userSearch", async () => {
const mockValue = "Im being mocked";
jest
.spyOn(useUserSearchMutation, "default")
.mockImplementation(() => () => mockValue);
act(() => {
render(<TestComponent />, container);
});
expect(container.textContent).toBe(mockValue);
});

How to use enzyme and jest to detect change React state

I have a simple React component, when user click the button I want to increase the internal value of state and render in an input button.
The component works, but I am not able to write a test with enzyme, basically the internal value is not being updated.
I think it is connected with setState being asynch, do you have any idea how to fix my test?
import * as React from 'react'
type TestCounterProps = Readonly<{
defaultValue: number
onClick: (value: number) => void
}>
export const TestCounter = ({ defaultValue, onClick }: TestCounterProps) => {
const [value, setValue] = React.useState(defaultValue)
const handleIncrease = () => {
setValue(value + 1)
onClick(value)
}
return (
<div>
<input value={value} readOnly />
<button onClick={handleIncrease}>Click to increase</button>
</div>
)
}
Test:
import * as React from 'react'
import { mount } from 'enzyme'
import { TestCounter } from './TestCounter'
describe('TestCounter', () => {
it('should increase counter by 1 when user click button', () => {
const cbClick = jest.fn()
const container = mount(<TestCounter defaultValue={0} onClick={cbClick} />)
const input = container.find('input')
const button = container.find('button')
button.simulate('click')
container.update()
expect(input.props().value).toBe(1) // issue here still 0 <<<
expect(cbClick).toBeCalledWith(1)
})
})
I have a similar example/component, I am going to past it here so could be useful as example:
import * as React from 'react'
type CounterProps = Readonly<{
initialCount: number
onClick: (count: number) => void
}>
export default function Counter({ initialCount, onClick }: CounterProps) {
const [count, setCount] = React.useState(initialCount)
const handleIncrement = () => {
setCount((prevState) => {
const newCount = prevState + 1
onClick(newCount)
return newCount
})
}
const handleIncrementWithDelay = () => {
setTimeout(() => {
setCount((prevState) => {
const newCount = prevState + 1
onClick(newCount)
return newCount
})
}, 2000)
}
return (
<div>
Current value: {count}
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleIncrementWithDelay}>Increment with delay</button>
</div>
)
}
The test:
import * as React from 'react'
import { mount, ReactWrapper } from 'enzyme'
import Counter from './Counter'
import { act } from 'react-dom/test-utils'
const COUNT_UPDATE_DELAY_MS = 2000
const waitForComponentToPaint = async (wrapper: ReactWrapper) => {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
wrapper.update()
})
}
describe('Counter', () => {
beforeAll(() => {
jest.useFakeTimers()
})
afterAll(() => {
jest.useRealTimers()
})
it('should display initial count', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
expect(wrapper.text()).toContain('Current value: 5')
expect(cbClick).not.toBeCalled()
})
it('should increment after "Increment" button is clicked', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
wrapper.find('button').at(0).simulate('click')
expect(wrapper.text()).toContain('Current value: 6')
expect(cbClick).toHaveBeenCalledWith(6)
})
it('should increment with delay after "Increment with delay" button is clicked', () => {
const cbClick = jest.fn()
const wrapper = mount(<Counter initialCount={5} onClick={cbClick} />)
waitForComponentToPaint(wrapper)
wrapper.find('button').at(1).simulate('click')
jest.advanceTimersByTime(COUNT_UPDATE_DELAY_MS + 1000)
expect(wrapper.text()).toContain('Current value: 6')
expect(cbClick).toHaveBeenCalledWith(6)
})
})

Categories

Resources