javascriptreactjsreact-hooksjestjsreact-testing-library

Can't successfully test a custom hook using useState and useEffect


While learning React, I'm trying to understand custom hooks.

I've created a simple one updating an internal "step" on creation, cleanup, and allows for external updates.

But I'm struggling to successfully implements its unit tests on "step" update and cleanup. It's as if the the returned "step" value is not updated anymore after the hook creation.

How can fix the tests? Did I miss some key concepts?

Here is the custom hook:

const useCustomHook = (stepToSet: string) => {
  const [step, setStep, ] = useState('');

  useEffect(() => {
    if (stepToSet) {
      setStep(stepToSet);
    }

    return () => {
      setStep('cleanup');
    };
  }, [stepToSet, ]);

  const externalCb = (externalStepToSet: string) => {
    setStep(externalStepToSet);
  };

  return {
    step,
    externalCb,
  };
};

Jest unit test:

import {
  act, renderHook, 
} from '@testing-library/react-hooks';
import { expect, } from 'chai';

describe('useCustomHook Hook', () => {

  it('step updates test', async () => {

    const {
      result,
      unmount,
      waitFor,
    } = renderHook(() => {
      return useCustomHook('created');
    });

    const {
      step,
      externalCb,
    } = result.current;

    expect(step).to.be.equal('created');

    act(() => externalCb('updated'));
    await waitFor(() => {});
    expect(step).to.be.equal('updated');

    act(() => unmount());
    await waitFor(() => {});
    expect(step).to.be.equal('cleaned');
  });
});

The test keeps failing with the message:

assert.strictEqual(received, expected)

Expected value to strictly be equal to:
  "updated"
Received:
  "created"

Message:
  expected 'created' to equal 'updated'

Difference:

- Expected
+ Received

- updated
+ created
...

The test still fails with the same message if I remove async and the await lines.


Solution

  • Issues

    Solution Suggestions

    import { act, renderHook } from "@testing-library/react";
    import { expect } from "chai";
    import { useCustomHook } from "./useCustomHook";
    
    describe("useCustomHook Hook", () => {
      it("step updates test", () => {
        const { result, unmount } = renderHook(useCustomHook, {
          initialProps: "created",
        });
    
        // Initial step value when mounted
        expect(result.current.step).to.be.equal("created");
    
        // Update state
        act(() => result.current.externalCb("updated"));
        expect(result.current.step).to.be.equal("updated");
    
        // Unmount
        act(() => unmount());
        // No update is expected here, I guess you could assert
        // nothing changed in the hook state
        expect(result.current.step).to.be.equal("updated");
      });
    });
    

    enter image description here

    If you need to waitFor any updates though, the correct usage would be similar to the following:

    import { act, renderHook, waitFor } from "@testing-library/react";
    import { expect } from "chai";
    import { useCustomHook } from "./useCustomHook";
    
    describe("useCustomHook Hook", () => {
      it("step updates test", async () => {
        const { result, unmount } = renderHook(useCustomHook, {
          initialProps: "created",
        });
    
        expect(result.current.step).to.be.equal("created");
    
        act(() => result.current.externalCb("updated"));
        await waitFor(() => {
          expect(result.current.step).to.be.equal("updated");
        });
    
        act(() => unmount());
        await waitFor(() => {
          expect(result.current.step).to.be.equal("updated");
        });
      });
    });