I am trying to unit test a custom hook using jest and react testing library in scenario where error is thrown but I am not able to catch the actual error message, here is my code so far:
my first hook:
import react from 'react';
const useFirstHook = () => {
//I will add conditional logic later
throw new Error('my custom error is thrown')
const test1 = 'I am test 1';
return {
test1
};
};
export default useFirstHook;
test.js
import React from 'react';
import { render } from '#testing-library/react';
import useFirstHook from './useFirstHook';
describe('useFirstHook', () => {
//I also tried adding jest.spy but no luck
/* beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
}); */
it('test 1', () => {
let result;
const TestComponent = () => {
result = useFirstHook()
return null;
};
render(<TestComponent />)
//expect()
});
});
my logic is to first create a hook, unit test it and then create component, add hook there and test that component with hook integration as well. what am I missing, or my approach is completely wrong ?
A good approach would be testing the component itself, that already contains the hook.
In case you consider that the hook needs to be test without the component, you can use #testing-library/react-hooks package, something like:
const useFirstHook = (shouldThrow = false) => {
// throw onmount
useEffect(() => {
if (shouldThrow) throw new Error('my custom error is thrown');
}, [shouldThrow]);
return {
test1: 'I am test 1'
};
};
describe('useFirstHook', () => {
it('should not throw', () => {
const { result } = renderHook(() => useFirstHook(false));
expect(result.current.test1).toEqual('I am test 1');
});
it('should throw', () => {
try {
const { result } = renderHook(() => useFirstHook(true));
expect(result.current).toBe(undefined);
} catch (err) {
expect(err).toEqual(Error('my custom error is thrown'));
}
});
});
Related
I am working on a React Native application and am very new to testing. I am trying to mock a hook that returns a true or false boolean based on the current user state. I need to mock the return value of the authState variable, and based on that, I should check if the component is rendered or not. But the jest mock is returning the same value only
useAuth.ts
export const useAuthState = () => {
const [authState, setAuthState] = useState<AuthState>();
useEffect(() => {
return authentication.subscribe(setAuthState);
}, []);
return authState;
};
MyComponent.tsx
export const MyComponent = () => {
const authState = useAuthState();
if (!authState) {
return null;
}
return <AnotherComponent />
}
MyComponent.test.tsx
import { MyComponent } from "./MyComponent"
jest.mock('../use-auth-state', () => {
return {
useAuthState: () => false,
};
});
const TestComponent = () => <MyComponent />
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
let testRenderer: ReactTestRenderer;
act(() => {
testRenderer = create(<TestComponent />);
});
const testInstance = testRenderer.getInstance();
expect(testInstance).toBeNull()
})
})
This is working fine. But, I am not able to mock useAuthState to be true as this false test case is failing. Am I doing it right? I feel like I am messing up something.
You want to change how useAuthState is mocked between tests, right? You can set your mock up as a spy instead and change the mock implementation between tests.
It's also a little more ergonomic to use the render method from react-testing-library. The easiest way would be to give your component a test ID and query for it. Something like the below
import { MyComponent } from "./MyComponent"
import * as useAuthState from '../use-auth-state';
const authStateSpy = jest.spyOn(useAuthState, 'default');
describe('MyComponent', () => {
it('Should return null if the authState is null', () => {
// you can use .mockImplementation at any time to change the mock behavior
authStateSpy.mockImplementation(() => false);
const { queryByTestId } = render(<MyComponent />;
expect(queryByTestId('testID')).toBeNull();
})
I need to test the following component that consumes a custom hook of mine.
import { useMyHook } from 'hooks/useMyHook';
const MyComponent = () => {
const myHookObj = useMyHook();
const handler = () => {
myHookObj.myMethod(someValue)
}
return(
<button onClick={handler}>MyButton</button>
);
};
This is my test file:
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: () => {
return {
myMethod: jest.fn(),
};
},
};
});
describe('<MyComponent />', () => {
it('calls the hook method when button is clicked', async () => {
render(<MyComponent {...props} />);
const button = screen.getByText('MyButton');
userEvent.click(button);
// Here I need to check that the `useMyHook.method`
// was called with some `value`
// How can I do this?
});
});
I need to check that the useMyHook.method was called with some value.
I also want to test it from multiple it cases and it might be called with different values on each test.
How can I do this?
This is how I was able to do it:
import { useMyHook } from 'hooks/useMyHook';
// Mock custom hook that it's used by the component
jest.mock('hooks/useMyHook', () => {
return {
useMyHook: jest.fn(),
};
});
// Mock the implementation of the `myMethod` method of the hook
// that is used by the Component
const myMethod = jest.fn();
(useMyHook as ReturnType<typeof jest.fn>).mockImplementation(() => {
return {
myMethod: myMethod,
};
});
// Reset mock state before each test
// Note: is needs to reset the mock call count
beforeEach(() => {
myMethod.mockReset();
});
Then, on the it clauses, I'm able to:
it (`does whatever`, async () => {
expect(myMethod).toHaveBeenCalledTimes(1);
expect(myMethod).toHaveBeenLastCalledWith(someValue);
});
I apologize if this question has been asked before. I did some searching but couldn't find an answer. I'm fairly new to React, so I may be missing something obvious.
I have inherited some TypeScript React code that includes a custom React hook to display a prompt when a user reloads or exits a page when there are edits on the page.
import { useState, useEffect } from 'react';
const initBeforeUnload = (showExitPrompt: boolean) => {
window.onbeforeunload = (event: BeforeUnloadEvent) => {
if (showExitPrompt) {
const e = event || window.event;
e.preventDefault();
if (e) {
e.returnValue = '';
}
return '';
}
};
};
// Hook
const useExitPrompt = (
bool: boolean
): [boolean, (showPrompt: boolean) => void] => {
const [showExitPrompt, setShowExitPrompt] = useState<boolean>(bool);
window.onload = () => {
initBeforeUnload(showExitPrompt);
};
useEffect(() => {
initBeforeUnload(showExitPrompt);
}, [showExitPrompt]);
useEffect(() => () => {
initBeforeUnload(false);
}, []);
return [showExitPrompt, setShowExitPrompt];
};
export default useExitPrompt;
No unit tests were included with this code when I inherited it, so I'm writing tests now. So far I've come up with the following:
/**
* #jest-environment jsdom
*/
import { renderHook, act } from '#testing-library/react-hooks';
import useExitPrompt from './useExitPrompt';
describe('useExitPrompt', () => {
it('should update the value of showExitPrompt', () => {
const { result } = renderHook(({ usePrompt }) => useExitPrompt(usePrompt),
{
initialProps: {
usePrompt: true
}
});
const [initialShowExitPrompt, setShowExitPrompt] = result.current;
expect(initialShowExitPrompt).toBe(true);
act(() => {
setShowExitPrompt(false);
});
const [updatedShowExitPrompt] = result.current;
expect(updatedShowExitPrompt).toBe(false);
});
});
This test passes, but it doesn't really get to the meat of what the hook is about: when showExitPrompt is true and the page is unloaded, is the exit prompt displayed? (In fact, it really only tests that the useState hook works, which is not what I want at all.) Unfortunately, I've run into a brick wall in trying to figure out how to test this; any suggestions?
I'm trying to write a test for the following:
import React from 'react'
import Popup from 'some-library'
const popupConfig = {
home: {
popupValue: 'Hello World',
popupValue: 'action',
popupMessage: 'Get Started'
},
settings: {
popupValue: 'Hello World',
popupValue: 'action',
popupMessage: 'Get Started'
}
}
const closePopup = () => {
Popup.closePopup()
}
const toggleNewPopup = () => {
Popup.togglePopup('some-popup')
}
const GetStartedPopup = ({ moduleName }) => {
if (!Object.keys(popupConfig).includes(moduleName)) return null
const {
popupValue = 'Hi there!',
popupStyle = 'warning',
popupMessage = 'Get Started',
popupBtnFunction = toggleNewPopup
} = popupConfig[moduleName]
return (
<Popup
popupValue={popupValue}
popupStyle={popupStyle}
popupBtnValue={popupMessage}
popupBtnStyle="neutral"
popupBtnFunction={popupBtnFunction}
xPopup={closePopup}
/>
)
}
export default GetStartedPopup
The objective of the test is to make sure that the closePopup and toggleNewPopup functions are called. I'm doing the following to do that for the closePopup function:
import React from 'react'
import { mount } from 'enzyme'
import { Popup } from 'some-library'
import GetStartedPopup from 'widgets/getStartedPopup'
describe('<GetStartedPopup/>', () => {
let wrapper
let props
beforeEach(() => {
props = {
page: 'home'
}
wrapper = mount(<GetStartedPopup {...props}/>)
})
it('should render the component without crashing', () => {
expect(wrapper).toBeDefined();
})
it('should call closePopup', () => {
const spy = jest.spyOn(wrapper.instance(), 'closePopup');
wrapper.instance().closePopup();
expect(spy).toHaveBeenCalledTimes(1);
})
afterEach(() => {
wrapper.unmount()
})
})
I went through the docs for spyOn and other SO threads that tackle issues like this but couldn't resolve how to test the closePopup and toggleNewPopup functions for my case here. When I run the test case written above I get this: TypeError: Cannot read property 'closePopup' of null. What would be the correct way to write the test to make sure that the two functions are called?
Funny that I ran into this myself at work in regards to wrapper.instance() doc
To return the props for the entire React component, use wrapper.instance().props. This is valid for stateful or stateless components in React 15.. But, wrapper.instance() will return null for stateless React component in React 16., so wrapper.instance().props will cause an error in this case.
As for the 3rd party library. You should be mocking any collaborators that your component uses.
import { Popup } from 'some-library';
describe('<GetStartedPopup />', () => {
let wrapper;
jest.mock('some-library', () => {
Popup: jest.fn(),
});
const initialProps = {
page: 'home'
};
const getStartedPopup = () => {
return mount(<GetStartedPopup {...initialProps});
};
beforeEach(() => {
Popup.mockClear()
wrapper = getStartedPopup();
};
it('should call closePopup', () => {
expect(Popup.closePopup()).toHaveBeenCalledTimes(1);
});
...
});
When testing an async react hook with #testing-library/react-hooks I see an error message. The error message mentions wrapping code in act(...) but I'm not sure where I should do this.
I have tried to wrap parts of the code in act(...) but each attempt leads to the test failing.
// day.js
import { useState, useEffect } from 'react';
import { getDay } from '../api/day';
export function useDay() {
const [state, set] = useState({ loading: false });
useEffect(() => {
let canSet = true;
set({ loading: true });
const setDay = async () => {
const day = await getDay();
if (canSet) {
set(day);
}
};
setDay();
return () => (canSet = false);
}, []);
return state;
}
// day.test.js
import { renderHook, act } from "#testing-library/react-hooks";
import { useDay } from "./day";
jest.mock("../api/day", () => ({
getDay: jest.fn().mockReturnValue({ some: "value" })
}));
describe.only("model/day", () => {
it("returns data", async () => {
const { result, waitForNextUpdate } = renderHook(() => useDay());
await waitForNextUpdate();
expect(result.current).toEqual({ some: "value" });
});
});
// test output
console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:102
Warning: An update to TestHook 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 is a known issue: https://github.com/testing-library/react-testing-library/issues/281
Before 16.9.0-alpha.0 React itself didn't handle the async stuff pretty good, so that has nothing to do with the testing library, really. Read the comments of the issue if you're interested in that.
You have two options now:
Update your React (& react-dom) to 16.9.0-alpha.0
Add a snippet (e. g. in your test setup file) to suppress that warning when console.log tries to print it:
// FIXME Remove when we upgrade to React >= 16.9
const originalConsoleError = console.error;
console.error = (...args) => {
if (/Warning.*not wrapped in act/.test(args[0])) {
return;
}
originalConsoleError(...args);
};