Enzyme/Jest test for useEffect() - reactjs

I have a component that looks like:
const PersonalGreeting = (): ReactElement => {
const [date, setDate] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date())
}, 60000)
return () => {
clearInterval(timer)
}
}, [])
return (
<div>{date}></div>
)
}
I am getting into an error that states Jest: "global" coverage threshold for functions (89%) not met: 88.73% and when I look at the logs, it says specifically my return () and clearInterval(timer) in useEffect() are not being tested. Here is a screenshot:
I honestly am pulling my hair out trying to figure out what they want me to test. Here is what I have tested:
describe('PersonalGreeting', () => {
const setTimeOfDay = jest.fn()
// eslint-disable-next-line
const useToggleMock: any = (initialState: string) => [
initialState,
setTimeOfDay
]
beforeEach(() => {
jest.spyOn(React, 'useState').mockImplementation(useToggleMock)
})
afterEach(() => {
jest.clearAllTimers()
})
it('renders component as expected', () => {
const wrapper = mount(
<TestWrapper>
<PersonalGreeting />
</TestWrapper>
)
expect(wrapper.text().length > 0).toBe(true)
})
it('Checks time every minute', () => {
jest.useFakeTimers()
mount(
<TestWrapper>
<PersonalGreeting />
</TestWrapper>
)
expect(setTimeOfDay).not.toBeCalled()
// 1 minute
jest.advanceTimersByTime(60000)
expect(setTimeOfDay).toHaveBeenCalledTimes(1)
})
})

You need to unmount your component, this will fire the return function in your useEffect()
See docs
Something like this
it('Should unmount component', () => {
const wrapper = mount(
<TestWrapper>
<PersonalGreeting />
</TestWrapper>
)
wrapper.unmount();
// continue with your expect here
})

Related

React component test function call in useEffect

I have this component:
export const CityForm: FC = () => {
const { state: widgetState } = useContext(WidgetContext);
const { dispatch: geocodingDispatch } = useContext(GeocodingContext);
const [city, setCity] = useState<string>('');
const debouncedCity = useDebouncedValue<string>(city, 800);
const fetchCallback = useCallback(() => {
getLocationCoords(geocodingDispatch)(widgetState.appid, debouncedCity);
}, [debouncedCity]);
useEffect(() => {
if (debouncedCity && widgetState.appid) {
fetchCallback();
} else {
clearLocationCoords(geocodingDispatch)();
}
return () => {
clearLocationCoords(geocodingDispatch)();
}
}, [debouncedCity]);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
setCity(e.target.value);
};
return (
<Input
data-testid={TEST_ID.CITY_FORM}
type={'text'}
value={city}
onChange={handleOnChange}
placeholder={"Type your City's name..."}
/>
);
};
I'm trying to write a test to check if getLocationCoords function will be called after user input.
this is the test:
it('should call getLocationCoords action after 800 miliseconds', async () => {
const dispatch = jest.fn(() => null);
const testCity = 'city';
const getLocationCoords = jest.fn(() => null);
jest.spyOn(useDebouncedValueModule, 'useDebouncedValue')
.mockImplementation((value) => value);
const component = render(
<ThemeProvider theme={lightTheme}>
<WidgetContext.Provider value={{ state: { appid: 'test' } as IWidgetState, dispatch: dispatch }}>
<GeocodingContext.Provider value={{ state: {} as IGeocodingState, dispatch: dispatch }}>
<CityForm />
</GeocodingContext.Provider>
</WidgetContext.Provider>
</ThemeProvider >
);
const input = await component.findByTestId(TEST_ID.CITY_FORM);
expect(input).toHaveValue('');
await userEvent.type(input, testCity);
expect(input).toHaveValue(testCity);
expect(getLocationCoords).toHaveBeenCalledTimes(1);
});
expect(getLocationCoords).toHaveBeenCalledTimes(1); Received number of calls: 0.
I cannot figure out why this happens. As the component works properly during manual testing.
Since the useDebouncedValue is mocked and returns the value instantaneously the debouncedCity within the component should update and trigger the useEffect
I had to add a spy on getLocationCoords like so:
it('should call getLocationCoords action with testCity as param after after input', async () => {
jest.spyOn(useDebouncedValueModule, 'useDebouncedValue')
.mockImplementation((value) => value);
const getLocationCoordsSpy = jest.spyOn(geocodingActions, 'getLocationCoords');
const component = render(
<ThemeProvider theme={lightTheme}>
<WidgetContext.Provider value={{ state: { appid: 'test' } as IWidgetState, dispatch: dispatch }}>
<GeocodingContext.Provider value={{ state: {} as IGeocodingState, dispatch }}>
<CityForm />
</GeocodingContext.Provider>
</WidgetContext.Provider>
</ThemeProvider >
);
const input = await component.findByTestId(TEST_ID.CITY_FORM);
expect(input).toHaveValue('');
act(() => {
fireEvent.change(input, { target: { value: testCity } });
});
expect(input).toHaveValue(testCity);
expect(getLocationCoordsSpy).toHaveBeenCalledTimes(1);

Jest - destructure property

export const AppLayout: React.FunctionComponent = React.memo(({ children }) => {
// Application main layout component name
AppLayout.displayName = getComponentName('App-Layout');
const { isAuthenticated } = useAuth();
const { sendRequest } = useApiService();
React.useEffect(() => {
const fetchData = async () => {
try {
...
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
describe('App General component', () => {
const useAuth = jest.fn();
const useApiService = jest.fn();
const isAuthenticated = true;
const props = {};
const renderComponent = () => render(
<AppLayout/>
);
it('should render without errors', () => {
renderComponent();
});
/**
* Validate current user exist in session
* #returns {boolean}
*/
const isAuthenticated = React.useCallback((): boolean => {
return Boolean(user);
}, [user]);
How can I set isAuthenticated to true so I can avoid the error
TypeError: Cannot destructure property 'isAuthenticated' of
const mockUseAuthIsAuthenticated = jest.fn(() => false);
const mockUseAuth = jest.fn(() => ({
isAuthenticated: mockUseAuthIsAuthenticated,
});
jest.mock("../hooks/useAuth", mockUseAuth);
describe('My test case', () => {
it(`should return authenticated=TRUE`, () => {
// Given
mockUseAuthIsAuthenticated.mockImplementationOnce(
() => true
);
// When
// assuming `render` comes from the react testing-library
render(<ComponentThatCallsTheHook />);
// Then
expect(mockUseAuthIsAuthenticated).toHaveBeenCalledOnce();
// ... more expectations
});
});
You should mock the useAuth hook like this:
jest.mock("yourUseAuthPath", () => ({
useAuth: () => ({
isAuthenticated: () => true
}),
}));
describe('App General component', () => {
...
}
n.b. You should replace the yourUseAuthPath with the correct path where you get the useAuth from. Example:
import { useAuth } from "yourUseAuthPath";
Some official docs here: https://jestjs.io/docs/mock-functions#mocking-partials

mock useDispatch followed by .then() with jest inside functional component

My scenario is just one step ahead of this existing question in stackoverflow
I have dispatch fn but with .then() followed by.
Component:
const Testing = props => {
const [counterData, setCounterData] = useState(0)
const resData = useSelector(state => state.test)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTestinData())
.then(res => {
console.log(res);
setCounterData(prev => prev + 1)
})
}, [])
return <div>
Testing component
<div>
Result - {resData.title}
</div>
<div>
Counter - {counterData}
</div>
</div>
}
Test file:
// const mockDispatch = jest.fn().mockResolvedValueOnce({json : async () => []})
const mockDispatch = jest.fn().mockImplementation(() =>
Priomise.resolve({title:'tets'}))
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch
}))
describe('<Testing />', function () {
const getComponent = (state) => <Provider store={store}>
<Testing />
</Provider>
it('testing success api', () => {
// window.fetch = jest.fn().mockResolvedValueOnce({title: 'testing title'})
render(getComponent())
screen.debug()
// expect(mockDispatch).toBeCalledTimes(1)
})
})
if am using just jest.fn() getting same error as well as with mock implemntaion.
Error screenshot
Something am missing in mock fn implementaion.
Plase help. Searched a lot but no luck.
Apparently Jest docs are a bit misleading about the possibility to use previously defined variables in a mock module factory: that is just not possible.
So the solution to your issue is just to move your mockDispatch implementation inside the module factory:
jest.mock('react-redux',
() => ({
...jest.requireActual('react-redux'),
useDispatch: () => jest.fn().mockImplementation(() =>
Promise.resolve({ title: 'test' }))
})
)

Test react forceUpdate custom hook useEffect/useState

I created a custom hook to force a component to update but I'm having issues figuring out how to write a unit test with jest.
This is the hook
function useForceUpdate(condition) {
const [, setState] = useState(0);
const forceUpdate = () => setState(1);
useEffect(() => {
if (condition) {
forceUpdate();
}
}, [condition]);
}
export default useForceUpdate;
I was able to successfully test this hook this way
import React from "react";
import useForceUpdate from "hooks/use-force-update";
const Component = ({ shouldUpdate }) => {
const hasUpdated = useForceUpdate(shouldUpdate);
return <div>{hasUpdated}</div>;
};
describe("useForceUpdate", () => {
let subject;
let props;
beforeEach(() => {
props = { shouldUpdate: true };
subject = memoize(() => mount(<Component {...props} />));
});
describe("when the condition is true", () => {
it("it calls forceUpdate", () => {
expect(
subject()
.find("div")
.text()
).toBe("1");
});
});
describe("when the condition is false", () => {
beforeEach(() => {
props = { shouldUpdate: false };
});
it("it does not call forceUpdate", () => {
expect(
subject()
.find("div")
.text()
).toBe("0");
});
});
});

How to mock React stateless currying function using Jest & Enzyme

I have this stateless React component:
...
const Providers = ({ onSelectFeedProvider, ... }) => {
const handleSelectFeedProvider = value => e => {
e.preventDefault();
onSelectFeedProvider({ target: { value } });
};
return {
<Row onClick={handleSelectFeedProvider(1)}>
...
</Row>
}
}
And the test:
import Row from 'components/Common/Row';
import Providers from './index';
jest.mock('components/Common/Row', () => 'Row');
let onSelectFeedProviderSpy = jest.fn();
let onSelectProviderSpy = jest.fn();
const initialProps = {
feedProvider: 0,
onSelectFeedProvider: () => onSelectFeedProviderSpy(),
selectedProvider: undefined,
onSelectProvider: () => onSelectProviderSpy()
};
const mockComponent = props => {
const finalProps = { ...initialProps, ...props };
return <Providers {...finalProps} />;
};
it('should call correctly', () => {
const wrapper = shallow(mockComponent());
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(onSelect).toHaveBeenCalledTimes(1);
});
How can I do to call the method correctly and pass the coverage? I think have tried all the possibilities. Any thoughts?
You don't have many options in this, one approach is to have onSelect injectable
const Component = ({onSelect}) => {
const handleSelect = value => e => {
e.preventDefault()
onSelect && onSelect({ target: { value } })
}
return <Row onClick={handleSelect(1)} />
}
Test
it('should call correctly', () => {
const spy = jest.fn()
const wrapper = shallow(mockComponent({onSelectProvider: spy}));
wrapper.find(Row).simulate('click', 'what do I have to do here');
expect(spy).toHaveBeenCalledTimes(1);
});

Resources