Skip to content

Instantly share code, notes, and snippets.

@harireddy7
Last active November 14, 2023 11:09
Show Gist options
  • Save harireddy7/8bd5c955a9919a1049f660f70004f90c to your computer and use it in GitHub Desktop.
Save harireddy7/8bd5c955a9919a1049f660f70004f90c to your computer and use it in GitHub Desktop.
Notes on React unit testing

Testing routing from one route to other route

// Add additional routes here to test routing
const UnitTestRouter = () => (
  <Routes>
    <Route
      path={'user/*'}
      element={<UserDashboard />}
    />
    <Route
      path={'user/add'}
      element={
        <AddUserProvider initialState={null}>
          <AddUserConfig />
        </AddUserProvider>
      }
    />
  </Routes>
);

const CustomRouterProvider = ({ initalRoute }: { initalRoute: string }) => (
  <MemoryRouter initialEntries={[initalRoute]}>
    <QueryClientProvider client={queryClient}>
      <ThemeProvider theme={theme}>
        <ErrorBoundary>
          <UnitTestRouter />
        </ErrorBoundary>
      </ThemeProvider>
    </QueryClientProvider>
  </MemoryRouter>
);

export const customRouterRender = (
  component: JSX.Element,
  initalRoute: string,
) =>
  render(component, {
    wrapper: () => <CustomRouterProvider initalRoute={initalRoute} />,
  });

In the test file, use customRouterRender to load the initial route

it('should go to /user/add route and come back to dashboard when cancel is clicked', async () => {
  const { getByText, findByPlaceholderText } = customRouterRender(<UserDashboard />, '/user');
  
  // rest of the test
  
});

jest.mock vs jest.spyOn

jest.mock:

  • Use this when you don't care about the internal implementation of the function/module.
  • All you do is return some value from that function using mockImplementation for your test cases.
  • This mock cannot be restored with original implementation in a specific test case we can wrap it in act() and render the component and then expect our cases

jest.spyOn:

  • Use this to mock a function/module same as jest.mock but one difference in this is you can restore the mocked implementation to its original implementation for specific test case.

example - jest.spyOn:

  import * as userModule from './user.api';
  
  // create a mock for the function in a module
  const mockFn = jest.spyOn(userModule, 'getUser');

  // add your implementation to that mock
  mockFn.mockImplementation(() => ({
      name: 'jonsnow',
      universe: 'game of thrones'
  }));

  it ('should display correct name', async () => {
    // success
    expect(userModule.getUser().name).toEqual('jonsnow');
    
    // restore the implementation to its original
    mockFn.restoreMock();
    
    // get user from original implementation
    expect(userModule.getUser()).not.toEqual('jonsnow')
    
  });

If a component makes api call on first render (like fetching a resource based on id from route params)

we can wrap it in act() and render the component and then expect our cases

example:

  it ('should load editable user', async () => {
    await act(async () => {
      render(<UserConfigForm userId={1} />);
    });
    // this will make sure to render the component
    // do all the api/state related updates until the DOM is stable

    expect(screen.queryByText('username').toEqual('barryallen')
  });

Waiting for something

waitFor is used to let the tests know to wait till the expectation inside is met.

example:

  it('should show name input field', async () => {
    const { getByText, getByDisplayValue } = render(<ConfigForm />);

    // waits till the expectation is met 👇
    await waitFor(() => {
      expect(getByText('Save')).not.toBeDisabled();
    });

    // expect other cases post waiting
    expect(getByText('Name')).toBeInTheDocument();
    expect(getByDisplayValue('Barry')).toBeInTheDocument();
  });

Mocking services:

Mock services with expected response instead of actually making the network call

example:

profile.api.ts export different APIs. mock the file and later mock required function's implementation with an expected response

// on top of test file
jest.mock('../../../../services/user/info/profile.api', () => ({
  getUserProfile: jest.fn(),
  getAdminProfile: jest.fn(),
}));
// inside a specific test
(getUserProfile as jest.Mock).mockImplementation(() => ({
    error: null,
    data: {
        id: 'user-id',
        name: 'barry allen',
        age: 27
    },
}));

this mock implementation will return the object when the test case hits the api (no network request is made) 🚀

Also make sure to restore the mocks after each test, so that next test will have its own mock implementation

beforeEach(() => {
    jest.resetAllMocks();
});

Helper function to get an element by name

const getSelector = (
  container: HTMLElement,
  key: keyof typeof formLables,
) => container.querySelector(`[name='${key}']`) as HTMLInputElement;

const { container } = renderWithProviders(<Component prop={value}>);
getSelector(container, 'username')

This will get the name=username input element

Helper function to fire onchange event on input

const fireInputChangeEvent = (
  container: HTMLElement,
  key: keyof typeof formLables,
  value: string,
) => {
  fireEvent.change(
    container.querySelector(`[name='${key}']`) as HTMLInputElement,
    { target: { value } },
  );
};

const { container } = renderWithProviders(<Component prop={value}>);
fireInputChangeEvent(container, 'username', 'barryallen')

This will emit an onchange event on name=username input with event.target.value=barryallen

If you have to test a button click event. And the click handler updates few states in the component, wrap the click event emitter in act

const saveButtonEl = getByText('Save');

await act(async () => {
    fireEvent.click(saveButtonEl);
});

// states changes are applied to DOM
// make assertions based on the state updates
const successEl = getByText('user saved')
expect(successEl).toBeInTheDocument()

wrapping code with act will make sure all the state updates are done and rendered them to DOM. Post this you can make your assertions

Issue: The current testing environment is not configured to support act(...)

Solution: testing-library/react-testing-library#1061 (comment)

Test a button click and show validation under the input field

import { fireEvent, screen, waitFor } from '@testing-library/react';

const { getByText } = renderWithProviders(<Component prop={value}>);

const saveButtonEl = getByText('Save');
fireEvent.click(saveButtonEl);

await waitFor(() => {
  const inputLabelEl = screen.queryByText('Username') as HTMLElement;
  expect(
    inputLabelEl.nextSibling?.firstChild?.lastChild?.textContent
  ).toEqual(
    'invalid username
  );
});

Test to type inside input and check the focus

import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

const { container } = renderWithProviders(<Component prop={value}>);

const emailEl = container.querySelector('#email') as HTMLInputElement;

// set focus inside email input
act(() => {
    emailEl.focus();
});

// type characters inside email input
userEvent.type(emailEl, 'c');

// check if focus is still on email input
expect(document.activeElement).toBe(emailEl);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment