React Testing Library - testing hooks with React.context

2.2k views Asked by At

I have question about react-testing-library with custom hooks

My tests seem to pass when I use context in custom hook, but when I update context value in hooks cleanup function and not pass. So can someone explain why this is or isn't a good way to test the custom hook ?

The provider and hook code:

// component.tsx
import * as React from "react";

const CountContext = React.createContext({
  count: 0,
  setCount: (c: number) => {},
});

export const CountProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  const value = { count, setCount };
  return <CountContext.Provider value={value}>{children}</CountContext.Provider>;
};

export const useCount = () => {
  const { count, setCount } = React.useContext(Context);

  React.useEffect(() => {
    return () => setCount(50);
  }, []);

  return { count, setCount };
};

The test code:

// component.spec.tsx
import * as React from "react";
import { act, render, screen } from "@testing-library/react";
import { CountProvider, useCount } from "./component";

describe("useCount", () => {
  it("should save count when unmount and restore count", () => {
    const Wrapper = ({ children }) => {
      return <ContextStateProvider>{children}</ContextStateProvider>;
    };

    const Component = () => {
      const { count, setCount } = useCount();
      return (
        <div>
          <div data-testid="foo">{count}</div>
        </div>
      );
    };

    const { unmount, rerender, getByTestId, getByText } = render(
      <Component />, { wrapper: Wrapper }
    );
    expect(getByTestId("foo").textContent).toBe("0");
    unmount();

    rerender(<Component />);
    // I Expected: "50" but Received: "0". but I dont understand why
    expect(getByTestId("foo").textContent).toBe("50");
  });
});
1

There are 1 answers

0
Lin Du On

When you call render, the rendered component tree is like this:

base element(document.body by default) -> container(createElement('div') by default) -> wrapper(CountProvider) -> Component

When you unmount the component instance, Wrapper will also be unmounted. See here.

When you rerender a new component instance, it just uses a new useCount hook and the default context value(you doesn' provide a context provider for rerender) in the useContext. So the count will always be 0. From the doc React.createContext:

The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.

You should NOT unmount the CountProvider wrapper, you may want to just unmount the Component. So that the component will receive the latest context value after mutate it.

So, the test component should be designed like this:

component.tsx:

import React from 'react';

const CountContext = React.createContext({
  count: 0,
  setCount: (c: number) => {},
});

export const CountProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  return <CountContext.Provider value={{ count, setCount }}>{children}</CountContext.Provider>;
};

export const useCount = () => {
  const { count, setCount } = React.useContext(CountContext);
  React.useEffect(() => {
    return () => setCount(50);
  }, []);

  return { count, setCount };
};

component.test.tsx:

import React, { useState } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { CountProvider, useCount } from './component';

describe('useCount', () => {
  it('should save count when unmount and restore count', () => {
    const Wrapper: React.ComponentType = ({ children }) => {
      const [visible, setVisible] = useState(true);
      return (
        <CountProvider>
          {visible && children}
          <button data-testid="toggle" onClick={() => setVisible((pre) => !pre)}></button>
        </CountProvider>
      );
    };

    const Component = () => {
      const { count } = useCount();
      return <div data-testid="foo">{count}</div>;
    };

    const { getByTestId } = render(<Component />, { wrapper: Wrapper });
    expect(getByTestId('foo').textContent).toBe('0');

    fireEvent.click(getByTestId('toggle')); // unmount the Component
    fireEvent.click(getByTestId('toggle')); // mount the Component again
    expect(getByTestId('foo').textContent).toBe('50');
  });
});

Test result:

 PASS  stackoverflow/67749630/component.test.tsx (8.46 s)
  useCount
    ✓ should save count when unmount and restore count (54 ms)

---------------|---------|----------|---------|---------|-------------------
File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------|---------|----------|---------|---------|-------------------
All files      |     100 |      100 |      80 |     100 |                   
 component.tsx |     100 |      100 |      80 |     100 |                   
---------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.988 s, estimated 9 s

Also take a look at this example: Codesandbox