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.
result
is holds a reference to the hook's current return value, but in your test you only access the initial value before effecting any changes, the later assertions don't have the updated hook value.result.current
each time after an act
.waitFor
calls appear to be superfluous since they are empty and could/should probably be removed. AFAIK act
is sufficient for triggering a re-render and hook update.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");
});
});
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");
});
});
});