React hook and component testing cookbook
Before Testing (Prep):
Separate behavior (hooks) from render to support testing patterns
In order to effectively test a component, it is recommended to separate out business logic from its render behavior/output. In practice, this means that for most components you will wind up with a hooks.js
file, separated from the component file.
hooks.js
file
self-import to allow mocking state (and extra hook methods if you have multiple) when testing hook behaviors
// allows mocking state modules from import * as module from './hooks';
Separate state accessors so that in tests you can individually control/verify the state of different values
const state = { // Separating state values into separately mock-able methods that forward // useState method allows much more effective mocking for testing. // The eslint-disable-line call is just to prevent needing to name // all of these `useMyStateField`. myStateField: (val) => React.useState(val), // eslint-disable-line myOtherStateField: (val) => React.useState(val), //eslint-disable-line } const useMyComponentData = (args) => { const [myStateField, setMyStateField] = module.state.myStateField(null); ... }
return callbacks for all behaviors in the component
only use useCallback if you have specific optimization needs
const useMyComponentData = (args) => { const [myStateField, setMyStateField] = module.state.myStateField(null); const clearMyStateField = () => setMyStateField(null); const setMyStateFieldCallback = (val) => () => setMyStateField(val); const performanceOptimizedCallback = useCallback(() => { doPerformanceIntensiveBehavior(myStateField); }, [myStateField]); return { // <Button onClick={clearMyStateField}>Clear</Button> clearMyStateField, // <Button onClick={setMyStateFieldCallback('orange')}>Orange</Button> setMyStateFieldCallback, // <Button onClick={performanceOptimizedCallback()}>Do The Thing!</Button> performanceOptimizedCallback, }; }
useEffect calls for behaviors on initialization or on state change
index.jsx
component file
Derive values directly from external hook components. This limits the number of sources that need to be mocked to test the component in isolation.
Derive all callbacks from hooks. This means that they can be mocked in tests, with names so that they show up appropriately in snapshots.
Internationalize all strings so that you can easily verify against them in render tests.
(even easier if you are passing them already translated from the component’s hook file)
Return simple JSX with minimal conditional renders. This reduces render tests to a set of conditional states that are validated with snapshots and targeted checks.
Good
Render values and callbacks from hooks or args
Bad
Complex value derivation (should be separated into hook code)
In-render callbacks (hard to test and snapshot. should be separated into hook code).
Perform maps where reasonable, but put overly complex maps into their own components.
If a map is over 5-10 lines of code, it could probably be broken into its own component.
Repeated patterns that should be broken into a map (make testing harder than it needs to be)
When mapping from static fields, separate for validation
Note: redux hooks
redux hooks can be defined at the app-level to further separate component behavior from data logic.
Testing Strategies
General
Mock Everything
Leverage setupTest for repeated mocks (react hooks, paragon, etc)
Be intentional about your test structure
use
describe
blocks to break up feature sets and conditions. Much easier to have a describe block for “on” state and one for “off” than to include that in all nested tests. Even easier when there are more than 3 states to check against.use
test
for statements andit
for behavioral assertionsit('calls initialize on load', () => {});
test('snapshot', () => {});
test('output includes most recent date', () => {});
Hook Tests hooks.test.js
Test the state connections separate from how they connect to the output. This lets you mock the state values for each test.
Test effects separately from output.
Mock hooks to return output based on their args (recipes below)
General Flow
Testing state
Mocking and testing State usage in output/behaviors
Testing events
Mock useEffect and just check against calls to it.
Be sure to clear all mocks between tests or these calls will just add up
check prerequisites and then run the callback and verify behavior
Sometimes it is helpful to add the test to verify the behavior doesn’t run without calling the callback
Testing memos and callbacks
Mock useMemo and useCallback to return their passed callback and prerequisites for easy testing.
Component File testing
Mock all incoming hooks and components
Ensure all logic (aside from conditional rendering) is separated into hooks.
Determine representational states that your component can be in
What are the options for the conditional statements in the render?
Derive a snapshot per state, and inspect each snapshot for relevant callbacks/pieces of data where relevant.
Ensure all mocked methods have names so they show up appropriately in snapshots
jest.mock().mockName('hooks.useMyHookValues')
If you can separate your messages into the hook file, it becomes extra-easy to inspect the snapshots in the tests for intended messages.
Because we no longer need to rely on mocks of formatted-message (which can get clunky) at that point, we can instead just mock the
translatedMessage
value from the hook and verify directly against it in the output.
Integration Test Patterns
Integration tests work well at the top level of the app to test a number of work paths in sequence with a minimal amount of mocking.
Make sure you un-mock everything listed in your setupTest file
generally you mainly want to mock api methods.
A simple pattern for mocking api events so that you can control when/if they return/fail
Generally for integration tests, react-testing-library’s renderer is more interrogate-able than enzyme’s and thus is preferred.
Where possible, find as few paths to flow through as you can, and verify as much as possible per path.
Helpful Mock Patterns
Internationalization setupTest.js
This mocks formatMessage to return a similar string for snapshot purposes. If any of the passed values are objects, produces a jsx output instead.