We recommend using React Testing Library for all your unit/integration testing needs.

// SimpleLoginForm.js
import React from 'react';

const SimpleLoginForm = ({ onSubmit }) => {
  const [error, setError] = React.useState('');
  function handleSubmit(event) {
    event.preventDefault();
    const {
      usernameInput: { value: username },
      passwordInput: { value: password },
    } = event.target.elements;
    if (!username) {
      setError('username is required');
    } else if (!password) {
      setError('password is required');
    } else {
      setError('');
      onSubmit({ username, password });
    }
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="usernameInput">Username</label>
          <input id="usernameInput" />
        </div>
        <div>
          <label htmlFor="passwordInput">Password</label>
          <input id="passwordInput" type="password" />
        </div>
        <button type="submit">Submit</button>
      </form>
      {error ? <div role="alert">{error}</div> : null}
    </div>
  );
};
export default SimpleLoginForm;
// SimpleLoginForm.unit.spec.jsx
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SimpleLoginForm from '../../components/SimpleLoginForm';

describe('my-application', () => {
  describe('SimpleLoginForm', () => {
    it('calls onSubmit with the username and password when submit is clicked', () => {
        const handleSubmit = sinon.spy();
        const screen = render(<SimpleLoginForm onSubmit={handleSubmit} />); // alternatively `const { getByLabelText, getByText } = render(<SimpleLoginForm onSubmit={handleSubmit} />);`
        const user = { username: 'user123', password: 'password123' };

        userEvent.type(screen.getByLabelText(/username/i), user.username);
        userEvent.type(screen.getByLabelText(/password/i), user.password);
        userEvent.click(getByText(/submit/i));

        expect(handleSubmit.callCount).to.equal(1); // alternatively `expect(handleSubmit.calledOnce).to.be.true()` works as well
        expect(handleSubmit.calledWith(user)).to.be.true(); // for more explicit testing we can use `calledWithExactly` in place of `calledWith`
    });
  });
});
CODE

We have written a "happy path" test for a SimpleLoginForm component. Let's break down this test

Setup

...
const handleSubmit = sinon.spy();
const screen = render(<SimpleLoginForm onSubmit={handleSubmit} />);
const user = { username: 'user123', password: 'password123' };
...
CODE
  1. We mock the handleSubmit function.

  2. We instantiate the SimpleLoginForm component, passing in the mocked handleSubmit as a prop.

  3. We use the render function from RTL to produce actual DOM nodes.

  4. We get our screen utility from the return value of render.

  5. We define the user data for reuse later in the test.

Note: In this example we gained access to our query functions through the return value from render. The global named screen import currently does not work in our test environment. Alternatively, you can destructure the return value to gain direct access to the RTL query functions.

DOM interactions and queries

...
userEvent.type(screen.getByLabelText(/username/i), user.username);
userEvent.type(screen.getByLabelText(/password/i), user.password);
userEvent.click(getByText(/submit/i));
...
CODE
  • Unit tests should be isolated

  • Components should use a unique label and text for each test.

Note: We can use the query functions that we destructured from render to find the input elements in our component by their label text. Testing Library provides a utility called userEvent that allows us to interact with the DOM nodes. We leverage the type interaction to enter our username and password into each respective field, then the click function to submit after querying with the submit button text.

Assertions

...
expect(handleSubmit.callCount).to.equal(1); // alternatively `expect(handleSubmit.calledOnce).to.be.true()` works as well
expect(handleSubmit.calledWith(user)).to.be.true(); // for more explicit testing we can use `calledWithExactly` in place of `calledWith`
...
CODE

To conclude this test we need to check that our onSubmit function fired and received the correct data.