How I usually test my ReactJS components
Tags: react, testing, typescript
Published: 2019-12-27 19:22:56


## Introduction

What I like about `@testing-library/react` is that it encourages testing on **what** users see instead of **how** a component works.

Today, I had a fun with it and I wanted to share an example component along with its tests.

The component is a login form. For simplicity reasons I skipped the password input.

## Show me the component first

To start with, I added the interface for its props.

```react
interface LoginFormProps {
  initialValues: { email: string };
  onSubmit?: (values: { email: string }) => void;
}
```

The component expects some `initialValues`, we keep it simple with just the `email` here, and the `onSubmit` callback that can be called with our new values. 

It renders a form with an input and a button element. Other than that, a form component _usually includes at least two event handlers and a state_.

The state's value derives from `initialValues` prop.

```react
const [values, setValues] = useState(initialValues);
```

As you might have guessed, one event handler will use the set state action that have been destructured from the [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook in order to update the form's state.

```react
function handleChange({ target }: React.ChangeEvent<HTMLInputElement>) {
  setValues(prev => ({ ...prev, [target.name]: target.value }));
}
```

The other event handler should be called when the form is submitted and should _call or not_ the `onSubmit` callback with the form's state.


```react
const handleSubmit = useCallback(
  (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    onSubmit?.(values);
  },
  [onSubmit, values]
);
```

When a callback has dependencies I create a memoized version of it with the help of [useCallback](https://reactjs.org/docs/hooks-reference.html#usecallback) hook.

#### Let's get dirty... 

Seriously, let's get a `dirty` variable in order to disable or not the button.

```react
const dirty = useMemo((): boolean => {
  return values.email !== initialValues.email;
}, [initialValues.email, values.email]);
```

Again, when I have variables with computed values I tend to memoize them.

#### That's all...

```react
// LoginForm.tsx

import React, { useCallback, useMemo, useState } from 'react';

export interface LoginFormProps {
  initialValues: { email: string };
  onSubmit?: (values: { email: string }) => void;
}

function LoginForm({
  initialValues,
  onSubmit
}: LoginFormProps): React.ReactElement {
  const [values, setValues] = useState(initialValues);

  const dirty = useMemo((): boolean => {
    return values.email !== initialValues.email;
  }, [initialValues.email, values.email]);

  function handleChange({ target }: React.ChangeEvent<HTMLInputElement>) {
    setValues(prev => ({ ...prev, [target.name]: target.value }));
  }

  const handleSubmit = useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();

      onSubmit?.(values);
    },
    [onSubmit, values]
  );

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        onChange={handleChange}
        placeholder="Email"
        type="email"
        value={values.email}
      />
      <button disabled={!dirty} type="submit">
        Login
      </button>
    </form>
  );
}

export default LoginForm;
```

## Show me the tests

`@testing-library` helps us write _user-centric_ tests, thus meaning the **what** user sees I mentioned in the beginning.

Here are some things that we need to test for this component.

1. The user sees a form with an input and a button.
2. The input displays the correct values.
3. The button should be disabled when the form is not dirty.
4. The form is working.

There are a lot of ways to write tests. `jest` provides us a variety of matchers and `@testing-library` a lot of query helpers.

Here's what I've come up with for the first case.

```react
describe('LoginForm component', () => {
  it('renders correctly', () => {
    const initialValues = { email: '' };

    const { container } = render(<LoginForm initialValues={initialValues} />);

    expect(container.firstChild).toMatchInlineSnapshot(`
      <form>
        <input
          name="email"
          placeholder="Email"
          type="email"
          value=""
        />
        <button
          disabled=""
          type="submit"
        >
          Login
        </button>
      </form>
    `);
  });
});
```

A couple of things to note here, `render` is coming from `@testing-library/react` and it renders the component into a container `div` and appends it to `document.body`. 

`container` is that `div` and we _expect_ from the `firstChild` which is our form to match the inline snapshot.

Another way I would write this test would be:

```react
// ...
const {
  getByPlaceholderText,
  getByText
} = render(<LoginForm initialValues={initialValues} />);

expect(getByPlaceholderText('Email').toBeInTheDocument();
expect(getByText('Login').toBeInTheDocument();
// ...
```

For the second item in our list I wrote the following tests.

```react
describe('input element', () => {
  it('renders the default value', () => {
    const initialValues = { email: '' };

    const { getByPlaceholderText } = render(
      <LoginForm initialValues={initialValues} />
    );

    expect(getByPlaceholderText('Email')).toHaveValue('');
  });

  it('renders the correct value', () => {
    const initialValues = { email: '' };

    const { getByPlaceholderText } = render(
      <LoginForm initialValues={initialValues} />
    );

    fireEvent.change(getByPlaceholderText('Email'), {
      target: { value: 'laura.marshall@cowtown.io' }
    });

    expect(getByPlaceholderText('Email')).toHaveValue(
      'laura.marshall@cowtown.io'
    );
  });
});
```

`@testing-library`'s `render` returns a variety of queries such as `getByPlaceholderText` which gives as access to the elements they find.

`fireEvent` on the other hand simply fires DOM events.

For example the following code fires a change event on our email input `getByPlaceholderText('Email')` and sets its value to `laura.marshall@cowtown.io`.

```react
fireEvent.change(getByPlaceholderText('Email'), {
  target: { value: 'laura.marshall@cowtown.io' }
});
```

With that said, I tested that our input renders the initial value and also updates properly.

I then test the _accessibility_ of the user to the Login button.

I used another amazing query `getByText` to find my button and changed my input's state by firing an event like my previous test.

```react
describe('submit button', () => {
  it('is disabled when the form is not dirty', () => {
    const initialValues = { email: 'laura.marshall@cowtown.io' };

    const { getByText } = render(<LoginForm initialValues={initialValues} />);

    expect(getByText('Login')).toBeDisabled();
  });

  it('is enabled when the form is dirty', () => {
    const initialValues = { email: '' };

    const { getByPlaceholderText, getByText } = render(
      <LoginForm initialValues={initialValues} />
    );

    fireEvent.change(getByPlaceholderText('Email'), {
      target: { value: 'laura.marshall@cowtown.io' }
    });

    expect(getByText('Login')).toBeEnabled();
  });
});
```

Finally I tested the button's functionality.

I created a mock function for my submit handler and tested that it is called with our new values when the Login button is pressed.

```react
describe('submit button', () => {
  // previous tests

  it('calls handleSubmit with the correct values', () => {
    const initialValues = { email: '' };
    const handleSubmit = jest.fn();

    const { getByPlaceholderText, getByText } = render(
      <LoginForm initialValues={initialValues} onSubmit={handleSubmit} />
    );

    fireEvent.change(getByPlaceholderText('Email'), {
      target: { value: 'laura.marshall@cowtown.io' }
    });

    fireEvent.click(getByText('Login'));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'laura.marshall@cowtown.io'
    });
  });
});
```