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 and it for behavioral assertions

      • it('calls initialize on load', () => {});

      • test('snapshot', () => {});

      • test('output includes most recent date', () => {});

    •  

Hook Tests hooks.test.js

  1. Test the state connections separate from how they connect to the output. This lets you mock the state values for each test.

  2. Test effects separately from output.

  3. 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

  1. Mock all incoming hooks and components

  2. Ensure all logic (aside from conditional rendering) is separated into hooks.

  3. Determine representational states that your component can be in

    1. What are the options for the conditional statements in the render?

  4. Derive a snapshot per state, and inspect each snapshot for relevant callbacks/pieces of data where relevant.

    1. Ensure all mocked methods have names so they show up appropriately in snapshots

      1. jest.mock().mockName('hooks.useMyHookValues')

  5. If you can separate your messages into the hook file, it becomes extra-easy to inspect the snapshots in the tests for intended messages.

    1. 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.

React and react-redux Hooks setupTest.js

Moment (for easier dates) setupTest.js

Mocking localStorage in setupTest.js

Mocking Components (and nested components) setupTest.js

for Nested components (Form, Form.Checkbox are both components) testUtils.js and setupTest.js

My preferred general-use state-mocking tool (testUtils.js)