I am experimenting with unit tests of react hooks using "React context dependency injection" to be able to better apply test-driven development (after reading two posts about it https://blog.testdouble.com/posts/2021-03-19-react-context-for-dependency-injection-not-state/ and https://medium.com/@matthill8286/dependency-injection-in-react-a-good-guide-with-code-examples-4afc8adc6cdb).
Now I want to fake/mock a hook's reactive return value in order to test another hook and verify that it reacts to changes in its dependencies. I have prepared a stackblitz example: https://stackblitz.com/edit/vitejs-vite-nwro2m?file=src%2FUseUsername.ts
// DependencyContext.ts
import { createContext } from 'react';
export interface Dependencies {
useUserId: () => string;
}
export const DependencyContext = createContext<Dependencies>(null!);
The hook I want to test is created with a factory in order to inject some preconfigured function that returns a username to a given userId. It also uses useUserId returned from the DependencyContext.
// UseUsername.ts
import { useContext } from 'react';
import { DependencyContext } from './DependencyContext';
export function createUseUsername(userNameApi: (userId: string) => string) {
return function () {
const { useUserId } = useContext(DependencyContext);
const userId = useUserId();
return userNameApi(userId);
};
}
Using a factory and the DependencyContext allows for tests to be contained, the hook to be isolated, and no modules required to mock. In the test below, I want to verify that the hook updates its value when the userId changes.
// UseUsername.test.tsx
import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { createUseUsername } from './UseUsername';
import { DependencyContext } from './DependencyContext';
describe('UseUsername', () => {
test('it returns correct name for a given userId', () => {
function usernameApiFake(userId: string): string {
const users: Record<string, string> = {
userA: 'usernameA',
userB: 'usernameB',
};
return users[userId] ?? '';
}
let userId = 'userA';
const useUserIdFake = () => userId;
const { result, rerender } = renderHook(
createUseUsername(usernameApiFake),
{
wrapper: ({ children }) => (
<DependencyContext.Provider value={{ useUserId: useUserIdFake }}>
{children}
</DependencyContext.Provider>
),
}
);
expect(result.current).toBe('usernameA');
act(() => (userId = 'userB'));
rerender(); // I don't want to have to call a rerender
expect(result.current).toBe('usernameB');
});
});
The problem is that the hook does not update, if I change the userId. I have to explicitly rerender the hook.
My questions:
- Is there a way to make
userIdreactive, so that the rendered hook automatically rerenders ifuserIdis changed? - Or is explicitly calling
rerenderequivalent and enough to unit test this hook?