Testing a simple component with React Testing Library
Tags: javascript, react, testing, webdev
Published: 2021-10-22 08:40:53

In this post join me as I use React Testing Library to test a simple React component. Apart from applying the testing library capabilities I hope to pass some of my testing methodologies along the way. As always, my example will be conducted on my [WordSearch game which](https://word-search-react-game.netlify.app/) I’m experimenting on.

The component I’ve chosen is the **AddWord**  - a simple component with the purpose of adding a new word to the word search game.
This component provides an input field, an “Add” button, and of course some inner logic for word validation and interaction with the external application.
I would like to test this logic using the [React Testing library](https://testing-library.com/docs/react-testing-library/intro/), which has a solid principle of mimicking the user’s interactions and focusing on that instead of the implementation details.

Although I’m not a big fan (to say the least) of the test-after methodology, in this one I will write the tests after the implementation is already done. Who knows, I might even reveal some bugs on the way...

Let’s start
This is our component, written in typescript but don’t be alarmed :) you can see how I converted it from JS in [this post](https://dev.to/mbarzeev/converting-a-react-component-to-typescript-15cl):

```javascript
import React, {ChangeEventHandler, MouseEventHandler, RefObject, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';
 
interface IAddWordProps {
   onWordAdd?: (value: string | undefined) => void;
}
 
const AddWord = ({onWordAdd}: IAddWordProps) => {
   const inputEl: RefObject<HTMLInputElement> | null = useRef(null);
   const [newWord, setNewWord] = useState('');
   const [disable, setDisable] = useState(true);
 
   const onAddClicked: MouseEventHandler<HTMLButtonElement> = () => {
       onWordAdd?.(newWord);
       setNewWord('');
   };
 
   const onChange: ChangeEventHandler<HTMLInputElement> = ({currentTarget: {value}}) => {
       setNewWord(value);
       // A word is valid if it has more than a single char and has no spaces
       const isInvalidWord: boolean = value.length < 2 || /\s/.test(value);
       setDisable(isInvalidWord);
   };
 
   return (
       <>
           <input
               type="text"
               name="new"
               required
               pattern="[Bb]anana|[Cc]herry"
               ref={inputEl}
               placeholder="Add word..."
               value={newWord}
               onChange={onChange}
           />
           <button onClick={onAddClicked} disabled={disable}>
               <Add />
           </button>
       </>
   );
};
 
export default AddWord;
```

We have 2 component states: **newWord** and **disabled**. It might very be that the disabled state is redundant, but you know what, I will attempt this refactor when I will complete the tests and then I will make sure that nothing has been broken.

Before I start, I would like to launch the test runner and set it on watch mode so I won’t need to keep on refreshing it. Since this project was created using [create-reach-app](https://github.com/facebook/create-react-app) the Jest runner and React testing library are already installed and configured, so all I need to do is run `npm run test` and I’m set (but if you don’t have it just follow the instructions on React testing lib and you should be good to go).

First I will render the component and see that it’s actually on screen.
I will create a new test file called: **AddWord.test.js** and write our first test with a dummy assertion just to make sure everything works well. Note that the test file is not on typescript for I would like to focus on the actual testing at the moment:

```javascript
describe('AddWord component', () => {
   it('should render the component onto the screen', () => {
       expect(true).toBeTruthy();
   });
});
```

Nice, tests are running, all passes.

Now I will attempt to render the component and my assertion will be checking if the input and button are on the screen. One of the better ways, IMO, of doing these sorts of screen queries is by applying a test ID to the component which provides a sort of an abstraction layer between the actual component and its representation for tests. You do that by adding a “data-testid” attribute to the component. 
I will give the different controls in the component a unique id:

```javascript
<>
           <input
               type="text"
               name="new"
               required
               pattern="[Bb]anana|[Cc]herry"
               ref={inputEl}
               placeholder="Add word..."
               value={newWord}
               onChange={onChange}
               data-testid="add-word-input"
           />
           <button onClick={onAddClicked} disabled={disable} data-testid="add-word-button">
               <Add />
           </button>
       </>
```

> Note: I am aware that testId is the last resort according to the recommendations, but I think that tightly coupling language-based queries (byText, byLabelText, byAltText, byPlaceholderText, etc.) are more fragile and limiting when you wish to test other languages, for instance.
More so, when you write e2e automatons you find that having a non-dependent identifier makes life a lot easier. So If you're going to do that for e2e test, why not use the same data-testId for both purposes.
This is a discussion that also took place on [twitter with Kent C. Dodds](https://twitter.com/mattibarzeev/status/1451495435789537402) and I might submit a "call" just to make sure I understand what he's suggesting as a solution there, while duplicating the test is not an option.

I usually don’t like having multiple assertions in a single test cause I think it makes it hard to maintain and kinda “blurs” out the purpose of a single unit test, but here we can have both assertions since there is no harm in that. My test looks like this now:

```javascript
it('should render the component onto the screen', () => {
       render(<AddWord />);
       expect(screen.getByTestId('add-word-input')).toBeInTheDocument();
       expect(screen.getByTestId('add-word-button')).toBeInTheDocument();
   });
```

BTW - the ability to make these sort of DOM assertions come from @testing-library/jest-dom support. You don't see the import in the file itself since create-react-app imports it on the setupTests.js for all tests.
(Thanks Matan Borenkraout :+1:)

Now we know that the initial state of the AddWord component has the “Add” button disabled, since you cannot add an empty word, right? Let’s check that as well - 
To make sure that my tests are not “lying” to me I like to assert the opposite of what I’m interested in, to be certain that my tests are not passing for some other, overlooked, Reason. Something like this:

```javascript
it('should have the "Add" button disabled when initialized', () => {
       render(<AddWord />);
 
       expect(screen.getByTestId('add-word-button')).toBeEnabled();
   });
```

Notice that I expect it to be enabled though I know it should be disabled, and Jest is quick to inform me on that:

```bash
expect(element).toBeEnabled()
 
    Received element is not enabled:
      <button data-testid="add-word-button" disabled="" />
```

Ok, now that we know our test is solid, let’s make the right assertion:

```javascript
it('should have the "Add" button disabled when initialized', () => {
       render(<AddWord />);
 
       expect(screen.getByTestId('add-word-button')).toBeDisabled();
   });
```

Now that I tested that I would like to test that when I enter an input the “Add” button becomes enabled. Here I’m also using 2 assertions - the first one to make sure that the button is disabled when the component is rendered for the first time, and the second one to make sure it becomes enabled when there is a valid input. I do that since I what to be sure that the button is not enabled at start for any unexpected reason:

```javascript
it('should enable the "Add" button when a valid input is entered', () => {
       render(<AddWord />);
 
       expect(screen.getByTestId('add-word-button')).toBeDisabled();
 
       const input = screen.getByTestId('add-word-input');
       fireEvent.change(input, {target: {value: 'matti'}});
 
       expect(screen.getByTestId('add-word-button')).toBeEnabled();
   });
```

Here we are simulating a “change” event which I believe is sufficient for this test, but there is an option to simulate the actual typing as well.

Cool. moving on to the validation part of the component. Since this is “test-after” we need to read the already implemented logic and derive the tests from it. 
First thing we would like to check is that when the input is less than 2 chars, the “Add” button is still disabled:

```javascript
it('should have the "Add" button disabled if the input is less than 2 chars', () => {
       render(<AddWord />);
 
       const input = screen.getByTestId('add-word-input');
       fireEvent.change(input, {target: {value: 'm'}});
 
       expect(screen.getByTestId('add-word-button')).toBeDisabled();
   });
```

Another thing that we would like to check is that if the word entered contains spaces, the “Add” button should turn disabled:

```javascript
it('should have the "Add" button disabled if the input contains spaces', () => {
       render(<AddWord />);
 
       const input = screen.getByTestId('add-word-input');
       fireEvent.change(input, {target: {value: 'm atti'}});
 
       expect(screen.getByTestId('add-word-button')).toBeDisabled();
   });
```

Nice :)

I think that in this point we covered the entire logic of the component. You know what? Let’s run the coverage check and see where we are at, with `npm run test -- --coverage`.
Oh my, it appears that I neglected something:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x72byry406b2h43h1vfh.png)
 
So these marks tell me that lines 14-15 are not covered, which are the lines inside this method:

```javascript
const onAddClicked: MouseEventHandler<HTMLButtonElement> = () => {
       onWordAdd?.(newWord);
       setNewWord('');
   };
```

True, I didn’t check what happens when clicking the “Add” button. Let’s do this now.
We will create a spy method, which is a Jest special method that you can “spy” on and see how many times it was called and with what argument (among other features). Then we will fill the input with a valid value, click the add button and expect that the spied handler method will be called with the value we’ve entered. Here how it looks:

```javascript
it('should call the onWordAdd handler (if exists) with the new word upon clicking the "Add" button', () => {
       const onWordsAddSpy = jest.fn();
       const inputValue = 'matti';
 
       render(<AddWord onWordAdd={onWordsAddSpy} />);
 
       const input = screen.getByTestId('add-word-input');
       const addButton = screen.getByTestId('add-word-button');
 
       fireEvent.change(input, {target: {value: inputValue}});
       fireEvent.click(addButton);
 
       expect(onWordsAddSpy).toHaveBeenCalledWith(inputValue);
   });
```

Another thing we should check is that once the button is clicked, the value in the input should be cleared. You guessed right - it is another test:

```javascript
it('should clear the input upon clicking the "Add" button', () => {
       render(<AddWord />);
 
       const input = screen.getByTestId('add-word-input');
       const addButton = screen.getByTestId('add-word-button');
 
       fireEvent.change(input, {target: {value: 'matti'}});
       fireEvent.click(addButton);
 
       expect(input.value).toBe('');
   });
```

Sweet. Now let’s check the coverage again:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbhe7zxshlr64ififsk0.png)
 
100% coverage :)

###Bonus
Now remember that I talked about the possibility that the “disabled” state might be redundant? Now I can start refactoring it with the assurance that my tests supply. Easy! After the change the component looks like this now:

```javascript
import React, {ChangeEventHandler, MouseEventHandler, RefObject, useRef, useState} from 'react';
import Add from '@material-ui/icons/Add';
 
interface IAddWordProps {
   onWordAdd?: (value: string | undefined) => void;
}
 
const AddWord = ({onWordAdd}: IAddWordProps) => {
   const inputEl: RefObject<HTMLInputElement> | null = useRef(null);
   const [newWord, setNewWord] = useState('');
 
   const onAddClicked: MouseEventHandler<HTMLButtonElement> = () => {
       onWordAdd?.(newWord);
       setNewWord('');
   };
 
   const onChange: ChangeEventHandler<HTMLInputElement> = ({currentTarget: {value}}) => {
       setNewWord(value);
   };
 
   // A word is valid if it has more than a single char and has no spaces
   const isInvalidWord: boolean = newWord.length < 2 || /\s/.test(newWord);
 
   return (
       <>
           <input
               type="text"
               name="new"
               required
               pattern="[Bb]anana|[Cc]herry"
               ref={inputEl}
               placeholder="Add word..."
               value={newWord}
               onChange={onChange}
               data-testid="add-word-input"
           />
           <button onClick={onAddClicked} disabled={isInvalidWord} data-testid="add-word-button">
               <Add />
           </button>
       </>
   );
};
 
export default AddWord;
```

As always, if you have any ideas on how to make this better or any other technique, be sure to share with the rest of us!

Cheers

*Hey! If you liked what you've just read be sure to also visit me on twitter* :) **<a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>** :beers:

<small><small><small>Photo by <a href="https://unsplash.com/@thisisengineering?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">ThisisEngineering RAEng</a> on <a href="https://unsplash.com/s/photos/software-testing?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></small></small>