Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Formik form submission with react-testing library

I am looking to fire a submit handler for a LoginForm. However, for some reason, instead of my mock function being called, the actual handler for the component gets fired (calling an external api). How can I ensure that my mock handler gets called instead?

The three components of interest are below (The presentational, container and the test suite)

LoginForm.js

import { Formik, Form, Field } from 'formik';
import { CustomInput } from '..';

const LoginForm = ({ initialValues, handleSubmit, validate }) => {
  return (
    <Formik
      initialValues={initialValues}
      validate={validate}
      onSubmit={handleSubmit}
    >
      {({ isSubmitting, handleSubmit }) => {
        return (
        <Form onSubmit={handleSubmit}>
          <div className="d-flex flex-column justify-content-center align-items-center">
            <Field
              data-testid="usernameOrEmail"
              type="text"
              name="identifier"
              placeholder="Username/Email"
              component={CustomInput}
              inputClass="mb-4 mt-2 text-monospace"
            />
            <Field
              data-testid="login-password"
              type="password"
              name="password"
              placeholder="Password"
              component={CustomInput}
              inputClass="mb-4 mt-4 text-monospace"
            />
            <button
              data-testid="login-button"
              className="btn btn-primary btn-lg mt-3 text-monospace"
              type="submit"
              disabled={isSubmitting}
              style={{ textTransform: 'uppercase', minWidth: '12rem' }}
            >
              Submit
            </button>
          </div>
        </Form>
      )}}
    </Formik>
  );
};

export default LoginForm;

LoginPage.js

import React, { useContext } from 'react';
import { loginUser } from '../../services';
import { userContext } from '../../contexts';
import { loginValidator } from '../../helpers';
import { setAuthorizationToken, renderAlert } from '../../utils';
import LoginForm from './login-form';

const INITIAL_VALUES = { identifier: '', password: '' };

const LoginPage = props => {
  const { handleUserData, handleAuthStatus } = useContext(userContext);

  const handleSubmit = async (values, { setSubmitting }) => {
    try {
      const result = await loginUser(values);
      handleAuthStatus(true);
      handleUserData(result.data);
      setAuthorizationToken(result.data.token);
      props.history.push('/habits');
      renderAlert('success', 'Login Successful');
    } catch (err) {
      renderAlert('error', err.message);
    }
    setSubmitting(false);
  };

  return (
    <LoginForm
      initialValues={INITIAL_VALUES}
      validate={values => loginValidator(values)}
      handleSubmit={handleSubmit}
    />
  );
};

export default LoginPage;

LoginPage.spec.js

import React from 'react';
import { cleanup, getByTestId, fireEvent, wait } from 'react-testing-library';
import { renderWithRouter } from '../../../helpers';
import LoginPage from '../login-page';

afterEach(cleanup);
const handleSubmit = jest.fn();

test('<LoginPage /> renders with blank fields', () => {
  const { container } = renderWithRouter(<LoginPage />);

  const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
  const passwordNode = getByTestId(container, 'login-password');
  const submitButtonNode = getByTestId(container, 'login-button');

  expect(usernameOrEmailNode.tagName).toBe('INPUT');
  expect(passwordNode.tagName).toBe('INPUT');
  expect(submitButtonNode.tagName).toBe('BUTTON');
  expect(usernameOrEmailNode.getAttribute('value')).toBe('');
  expect(passwordNode.getAttribute('value')).toBe('');
});

test('Clicking the submit button after entering values', async () => {
  const { container } = renderWithRouter(<LoginPage handleSubmit={handleSubmit} />);

  const usernameOrEmailNode = getByTestId(container, 'usernameOrEmail');
  const passwordNode = getByTestId(container, 'login-password');
  const submitButtonNode = getByTestId(container, 'login-button');

  fireEvent.change(usernameOrEmailNode, { target: { value: fakeUser.username }});
  fireEvent.change(passwordNode, { target: { value: fakeUser.password }});
  fireEvent.click(submitButtonNode);

  await wait(() => {
    expect(handleSubmit).toHaveBeenCalledTimes(1);
  });


  expect(usernameOrEmailNode.tagName).toBe('INPUT');
  expect(passwordNode.tagName).toBe('INPUT');
  expect(submitButtonNode.tagName).toBe('BUTTON');
  expect(usernameOrEmailNode.getAttribute('value')).toBe('');
  expect(passwordNode.getAttribute('value')).toBe('');
});```

like image 668
Oreoluwa Aboluwarin Avatar asked May 24 '26 20:05

Oreoluwa Aboluwarin


1 Answers

To answer your question, you will need to first make the handleSubmit constant accessible outside LoginPage.js so that it may be mocked and then tested. For example,

LoginPage.js

export const handleSubmit = async (values, { setSubmitting }) => {
 ... code to handle submission
})

And in your tests - LoginPage.spec.js

jest.unmock('./login-page');
import LoginPage, otherFunctions from '../login-page'
otherFunctions.handleSubmit = jest.fn();

...
test('Clicking the submit button after entering values', () => {
  ...
  fireEvent.click(submitButtonNode);
  expect(handleSubmit).toHaveBeenCalledTimes(1);
})

I hope the above fixes your problem.

But, going by the philosophy of unit testing, the above components must not be tested the way you are doing it. Instead your test setup should be like this -

  1. Add a new test file called LoginForm.spec.js that tests your LoginForm component. You would test the following in this -
    1. Check if all input fields have been rendered.
    2. Check if the correct handler is called on submit and with the correct parameters.
  2. The existing test file called LoginPage.spec.js would then only test if the particular form was rendered and then you could also test what the handleSubmit method does individually.

I believe the above would make your tests more clearer and readable too, because of the separation of concerns and would also allow you to test more edge cases.

like image 140
bitsapien Avatar answered May 26 '26 10:05

bitsapien