Testing

Custom Hooks - React

What are hooks?

export MyComponent = ({children}) => (
  <div className="MyComponent">
    {children}
  </div>
),
export MyComponent = ({text}) => {

  const [message, setMessage] = useState('');
  useEffect(() => {
    setMessage(text === 'Marco' 
      ? 'Polo' 
      : children);
  }, [text]);

  return (
    <div className="MyComponent">
      {children}
    </div>
  )
}

What are hooks?

export const useMessage = (text) => {

  const [message, setMessage] = useState('');
  useEffect(() => {
    setMessage(text === 'Marco' 
      ? 'Polo' 
      : children);
  }, [text]);

  return { message }
}

The first time that we saw a hook:

It has sense...

... but not at all

Something that can not be tested is not good implementation

And that brings us to the current situation:

export const useViewport = () => {

  const [viewport, setViewport] = useState(null);
    
  const handleResize = () => {
    if (window.innerWidth > 1200) {
      setViewport('extra-large')
    } else if (window.innerWidth > 992) {
      setViewport('large')
    } else if (window.innerWidth > 768) {
      setViewport('medium')
    } else if (window.innerWidth > 576) {
      setViewport('small')
    } else {
      setViewport('extra-small')
    }
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  })

  return { viewport };

}

If we want to test, we will have multiple aproachs:

  • First wrap in a component
  • Using a library to tests hooks
describe('useViewport', () => {
  it('changes the viewport', () => {
    
  });
});

Using a component

describe('useViewport', () => {
  const TestComponent = () => {
    const { viewport } = useViewport();
    return <span>{viewport}</span>
  }

  const fireResizeEvent = (width) => {
    window.innerWidth = width
    window.dispatchEvent(new Event('resize'))
  }

  it('changes the viewport to medium', () => {
    const expectedViewport = 'medium';
    const view = render(<TestComponent />);

    fireResizeEvent(800)
    view.rerender();
	expect(
      view.queryByText(expectedViewport)
    ).toBeInDocument();
  });
});

Create an specific component

Select by text in DOM

Test our hook depends on component

Using testing-library/react-hooks

Each mutation should be handled outside

Tools:

describe('useLocale', () => {
  it('changes the state', async () => {
    const newState = 'New state';
    const { result } = renderHook(() => useHook('Initial state'));

    await act(async () => {
      await result.current.setState('irrelevant state');
    });

    expect(result.current.state).toEqual(newState);
  });
});

The problem:

We are not exposing anything

export const useViewport = () => {

  const [viewport, setViewport] = useState(null);
    
  const handleResize = () => {
    if (window.innerWidth > 1200) {
      setViewport('extra-large')
    } else if (window.innerWidth > 992) {
      setViewport('large')
    } else if (window.innerWidth > 768) {
      setViewport('medium')
    } else if (window.innerWidth > 576) {
      setViewport('small')
    } else {
      setViewport('extra-small')
    }
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  })

  return viewport;

}
export const useViewport = () => {

  const [viewport, setViewport] = useState(null);
    
  const resizedTo = (width) => {
    if (width > 1200) {
      setViewport('extra-large')
    } else if (width > 992) {
      setViewport('large')
    } else if (width > 768) {
      setViewport('medium')
    } else if (width > 576) {
      setViewport('small')
    } else {
      setViewport('extra-small')
    }
  };

  return {viewport, resizedTo};

}
describe('useViewport', () => {
  it('changes the viewport', async () => {
    const { result } = renderHook(() => useViewport());
    
    act(() => {
      result.current.resizedTo(800)
    });

    expect(result.current.viewport).toEqual('medium');
  });
});
const MyComponent = () => {
  
  const {viewport, resizedTo} = useViewport();
  const handleResize = () => { resizedTo(window.innerWidth) };

  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    }
  })

  return (<>{// irrelevant code}</>)

}

Single responsability

Test help you to define a good architecture

useTDD('

')

This is not for test, is for define functionalities and think about the code

EXTRA ROUND:

Do you need fetch data?

export useProducts = () => {
  
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  // remember to expose your methods
  const getProducts = async () => {
    setLoading(true);
    try {
      setProducts(await productRepository.getProducts);
    } catch(error) {
      setErrorMessage(error.message)
    } finally {
      setLoading(false);
    }
  }

  return { products, getProducts, loading, error }
}
export useProducts = (repository = productRepository) => {
  
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState(null);
  
  // remember to expose your methods
  const getProducts = async () => {
    setLoading(true);
    try {
      setProducts(await repository.getProducts);
    } catch(error) {
      setErrorMessage(error.message)
    } finally {
      setLoading(false);
    }
  }

  return { products, getProducts, loading, error }
}
describe('useAsyncProduct', () => {
  it('retrieves products from repository', async () => {
  
    const product = buildProduct({});
    const productRepositoryMock = {
      getProducts: jest.fn(() => Promise.resolve([product])),
    };

    const { result } = renderHook(() => useProducts(productRepositoryMock));

    await act(async () => {
      await result.current.getProducts();
    });

    expect(result.current.products).toEqual([products]);
    expect(result.current.loading).toBeFalsy();
  });
});

Questions?

Thank you!

Testing custom hooks in React

By afergon

Testing custom hooks in React

  • 369