CodeMonkey
CodeMonkey

Reputation: 12424

Testing a custom hook with renderHook

I want to test a custom hook which was implemented as an helping function for code reuse with other hooks. It's calling useDispatch and useSelector in its implementation, along with saving data in the session storage:

 export function useCustomHook(key, obj)
  {
    let myObject = {
      somefield: obj.someField
    };
    sessionStorage.setItem(key, JSON.stringify(myObject));
    const dispatch = useDispatch();
    dispatch(actionCreator.addAction(key, myObject));
  }

And the test:

it('should have data in redux and session storage', () =>
{
  const obj = { 
    somefield: 'my val', 
  };
  renderHook(() => useCustomHook('some key', obj));
  let savedObj= JSON.parse(sessionStorage.getItem('some key'));
  expect(savedObj.someField).toBe('my val');
  renderHook(() => 
  {
    console.log("INSIDE");
    let reduxObj = useSelector(state => state.vals);
    console.log("THE OBJECT: " );
    console.log(reduxObj);
    expect(reduxObj).toBe(2); //just to see if it fails the test - it's not
  });
})

No matter what I try, the test only arrives to the "INSIDE" console log and does not print the "THE OBJECT: " console log. The test itself still passes so it's like the useSelector somehow stops the rest of the renderHook execution.

I'm guessing it's related to the fact that the test doesn't have a store connected... What can be done to test redux in this case?

Upvotes: 5

Views: 9647

Answers (2)

Liam
Liam

Reputation: 29654

You can write a simple extension of the documented redux renderWithProviders function. This looks like:


import React, { PropsWithChildren } from 'react'
import { render } from '@testing-library/react'
import type { RenderOptions } from '@testing-library/react'
import { configureStore } from '@reduxjs/toolkit'
import type { PreloadedState } from '@reduxjs/toolkit'
import { Provider } from 'react-redux';
import { renderHook } from '@testing-library/react';


export function renderHookWithProviders<
  Result,
  Props>(
    render: (initialProps: Props) => Result,
    {
      preloadedState = {},
      // Automatically create a store instance if no store was passed in
      store = configureStore({
        reducer: {
....
        },
        preloadedState
      }),
      ...renderOptions
    }: ExtendedRenderOptions = {}
  ) {
  function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
    return <Provider store={store}>{children}</Provider>
  }

  return { store, ...renderHook(render, { wrapper: Wrapper, ...renderOptions }) }
}

Notice the render method is replaced with renderHook from @testing-library/react. This can then be used simply in your tests:

describe('useGetRelated', () => {
  test('Return related', () => {
    const { result } = renderHookWithProviders(() => useGetRelated(), {
      preloadedState: {
        //initial state of redux
      }
    });


  });
});

The hooks testing library talk about this briefly as well. Though the solution there requires more boiler plate in each test, the redux advocated solution seems better IMO.

Upvotes: 6

Michael Peyper
Michael Peyper

Reputation: 6944

You'd need to provide a wrapper component that add the redux Provider with a store to connect to:

it('should have data in redux and session storage', () =>
{
  const obj = { 
    somefield: 'my val', 
  };

  const store = {} // create/mock a store
  const wrapper = ({ children }) => <Provider store={store}>{children}</Provider>

  renderHook(() => useCustomHook('some key', obj), { wrapper });
  let savedObj= JSON.parse(sessionStorage.getItem('some key'));
  expect(savedObj.someField).toBe('my val');
  renderHook(() => {
    console.log("INSIDE");
    let reduxObj = useSelector(state => state.vals);
    console.log("THE OBJECT: " );
    console.log(reduxObj);
    expect(reduxObj).toBe(2); //just to see if it fails the test - it's not
  }, { wrapper });
})

Just as a side note, the renderHook is catching the errors, which is why you aren't seeing them in your test, if you had tried to access result.current it would have thrown, and you could have seen it represented in result.error, but the usage here of not returning a value from a custom hook to be asserted against is quite unusual.

This will likely also cause you issues by having the assertion inside second renderHook call. You'll probably want to either return the value from the hook and assert outside, or assert the updated value in the redux store instead.

Upvotes: 1

Related Questions