Get Started with React Component Testing, Mocking, and Debugging
Tags: react, testing, componenttesting, frontend
Published: 2022-11-29 01:21:29

React component testing helps ensure components are performing the expected UI & UX behaviors. This post provides some tips to get started.

**Table of Contents**:

👉 What to test

👉 The render function, common queries & expects, and triggering events

👉 Mocking data, functions, and libraries (+ special cases for mocking Redux and React Router)

👉 Running the tests & debugging

## What to Test

Component testing should cover the component’s UI/UX behavior expectations, including how it handles different props or data input. The key here is testing the **behavior**, not the implementation. Easiest way to scaffold our tests is taking the product requirements and write them out in **plain English** that can be easily understood by product or project managers. We can start a testing block with “It should…” or “It should not…” to describe happy paths and fail paths perspectively.

Here’s an example of a scaffolded test file:

```tsx
// The `describe` blocks are used to explain the testing context,
// and the `it` blocks are used to lay out detailed behaviors
describe('data value is not provided', () => {
	it('should render a text input', () => {
	  // test details
	});
};

describe('data value is provided', () => {
	it('should render title if one is provided', () => {
	  // test details
	});

	it('should render a picture if one is provided', () => {
    // test details
	})
};

describe('give suggestions for data input', () => {
	it('should render suggestion titles', () => {
		// test details
	});
	
	it('should not allow users to pick more than one suggested items', () => {
    // test details
	});
}
```

In general, here are a few things component testing should cover:

- The rendering of the intended HTML elements
- The rendering of elements based on props
- The events being triggered as expected and with intended results
- The state of the app, for example loading and error states
- For styling, we don’t need to test the exact CSS (that’s a whole other category called visual testing), we should test style changes if it corresponds to prop change or user interaction.

## The Render Function and Basic Queries

Let me start with a basic example:

```tsx
import React from 'react';
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';

const makeProps = () => ({
  content: 'hiking',
});

describe('MyComponent', () => {
  const props = makeProps();

  it('should display content', () => {
    const { container } = render(<MyComponent {...props} />);
    expect(container).toHaveTextContent(props.content);
		expect(container.firstChild.nodeName).toBe('DIV');
  });
});
```

Here’s what we did:

- Use `render` from `@testing-library/react` to render the component into the virtual DOM and from there we can get the `container`, which is the rendered HTML component. We can also get a series of methods like `getByTestId`, we’ll look at those next.
- We use `expect` to express that we want the container to have the `content` we passed in as props and that it should be rendered in a `div` element.
- We use a `makeProps` helper to generate props. This is not required, but it’s helpful as the tests get complicated because we can reuse this helper and pass arguments based on different contexts to get different sets of props.

### Common Queries

Now let’s go through some basic queries we can utilize from the `render` function.

- There are three types of queries: `get(getAll)By-`, `query(queryAll)By-`, or `find(findAll)By-`:
    - In a lot of cases these three types of queries are interchangeable, however there’re scenarios where one is more suitable. For example accessing an element that may be null, we need to use `queryBy` because `getBy` would return error. If we want the error to be returned, we should use `getBy`. For `findBy`, it can be used to find the *first* element when multiple elements exist, it can also be used when needing to await changes in the DOM.
        - Example: Use `queryBy` to assert an element that **should not** be in the DOM
            
            ```tsx
            it('should not display the button', () => {
                const { queryByTestId } = render(<MyComponent {...props} />);
                const theButton = queryByTestId('the-button');
            
                expect(theButton).toBeNull();
            });
            ```
            
- There are a variety of elements we can query against, including *labelText*, *title*, *role*, *testId*, etc. Note that using `testId` should be the last resort because it is the least accessible property by the actual users.
- To find multiple elements, should use the `-all` queries. These queries will return us an array of elements. To access a specific element, we can use their index to access the array, or access them via `testId`.
- To get text content inside an HTML element, use `.innerHTML`
    
    ```jsx
    it('should display value up to 100 characters', () => {
      const { container, getByTestId } = render(<MyComponent {...props} />);
      const display = getByTestId('content').innerHTML;
    
      expect(container).toHaveTextContent(display);
      expect(display.length).toBe(100);
    });
    ```
    
- When needing to find elements by partial test, Regex comes in handy. For example, I needed to find by text ‘Worked here for…’ but the exact number of working years is dynamic:
    
    ```tsx
    const item = getByText(/Worked here for .*/);
    ```
    
    - We can also use Regex to assess the working years display is of a certain format:
        
        ```tsx
        expect(item.innerHTML).toMatch(/^Worked here for .*years(?:(.*)months)?/gi);
        ```
        
- To find an image, we can use `-ByAltText`, e.g.
    
    ```tsx
    it('should render avatar with the given url', async () => {
        const { getByAltText } = await render(<MyComponent />);
    
        const avatar = getByAltText('avatar');
    
        expect(image).toHaveAttribute('src', 'the_given_url');
    });
    ```
    

### Common Expects

- Expect to be in the document: `expect(element).toBeInTheDocument`
- Expect to have certain length:  `expect(items.length).toBe(5)`
- Expect a link is leading to the right url:
    
    ```tsx
    it('should render learn more button with the link to the info site', () => {
        const { getByText } = render(<MyComponent />);
        const link = getByText('Learn more');
    
        expect(link).toHaveAttribute('href', 'www.info.com');
        expect(link).toHaveAttribute('target', '_blank');
        expect(link).toHaveAttribute('rel', 'noopener noreferrer');
      });
    ```
    
- Expect text to be a certain format: `expect(text).toMatch(regex)`
- Expect function to have been called with arguments.
    
    ```jsx
    expect(myFunction).toHaveBeenCalledWith({
    	argOne: 700,
    	argTwo: 'some text',
    	callbackArg: expect.any(Function),
    });
    ```
    
- Expect the rest of props are being called if spreading props via `...restOfProps`
    
    ```jsx
    it('should pass through other props', () => {
        const { container } = render(<MyComponent data-testid="testID" />);
        expect(container.firstChild).toHaveAttribute('data-testid', 'testID');
    });
    ```
    
- Expect optional props can be passed in. For example, assert optional `style` is passed in:
    
    ```tsx
    it('should pass through additional style', () => { 
      const { container } = render(<MyComponent style={{ maxWidth: 500 }} />); 
      expect(container.firstChild).toHaveStyle('max-width: 500px'); 
    });
    ```
    

### Triggering Events

To tigger events, we use `fireEvents` from the react testing library. Example:

```jsx
import { render, fireEvent } from '@testing-library/react';

it('should fire authentication function when clicking sign in', () => {
  const { getByTestId } = render(<MyComponent />);
  const signInButton = getByTestId('sign-in-button');

  expect(signInButton).toBeInTheDocument();
  fireEvent.click(signInButton);

  expect(myAuthFunction).toHaveBeenCalled();
});
```

## Mocking

Since React component testing is testing for a certain component, it should be contained within the component’s own context. Therefore the component working as expected should not rely on the inner workings of the outside libraries or external functions utilized by the component. We need to mock those functions out to give expected outcome. Similarly, our tests should not rely on successful data fetching, so we need to mock out the data our tests would receive as well.

### Mock Data

‘Faker’ comes in as a handy library when mocking data. You can type data out yourself too, but using ‘faker’ puts the emphasis on the data format rather than the exact data content, which mimics actual user scenarios better. Here are a few handy use cases:

```jsx
import { faker } from '@faker-js/faker';

// Generate one random word
const word = faker.random.word();

// Generate random text
const content = faker.random.words(50); // optional word count argument

// Generate random image
const image = faker.image.avatar();

// Generate random uuid
const id = faker.datatype.uuid();

// Generate random first and last names
const name = faker.name.firstName(), faker.name.lastName()].join(' ');

// Generate random boolean value
const booleanValue = faker.datatype.boolean();

// Generate random number with precision, for example needing levels 100 | 200 | 300
const level = faker.random.number({ min: 100, max: 300, precision: 100 });
```

After mocking out the data, we can either feed it directly to our test components as props, or we can feed it as mock API response, to do so, use `mockReturnValue` or `mockReturnValueOnce` when needing multiple mock api calls within one test.

### Mock Functions

Mock functions let you spy on the behavior of a function our component is using but don’t need to test for its behavior directly.

- If the function result doesn’t impact the test, can just do: `const myFn = jest.fn()`
- To mock media functions, use `spyOn` and then restore it with `mockRestore` afterwards.
    
    ```jsx
    const playSub = jest.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(() => {});
    
    it('should play audio on click', () => {
      const { getByTitle } = render(<MyAudio id={faker.datatype.uuid()} />);
      const playButton = getByTitle('Play');
    
      fireEvent.click(playButton);
      expect(playSub).toHaveBeenCalled();
    
      playSub.mockRestore();
    });
    ```
    
- If need to mock for all tests, do mocks in `beforeAll` and cleanup in `afterAll`
    
    ```jsx
    beforeAll(() => {
      jest.mock(myFn, arg => ({ key: value }));
    });
    
    afterAll(() => {
      jest.restoreAllMocks();
    });
    ```
    

### Mock Libraries

Similar idea to mocking functions, if we use functions from a library, we want to mock it out so only the function output matters.

- Import the library we’re going to mock then use `mockImplementation`.
    
    ```tsx
    import lib from 'lib';
    
    jest.mock('lib');
    
    it('should work', () => {
    	lib.mockImplementation(() => 'output');
    });
    ```
    
- If mocking a module, need to get the path, not the name of the import.
    
    ```jsx
    // ❌ don't do this
    jest.mock('intersection');
    
    // ✅ do this
    jest.mock('lodash/intersection');
    ```
    
- If mocking a library function that needs initialization:
    
    ```tsx
    // ❌ don't do this
    jest.mock('lib', () => ({}));
    // ✅ do this
    jest.mock('lib', () => () => ({}));
    ```
    

### Special case: Mock Redux store

To test with Redux, we need to mock the store and render the test component with the provider and provided store. To solve this, we can a `renderWithStore` helper that will:

- Import our store
- Provide our test component with the needed redux provider wrapper
- Wraps the `render` function from the testing library
- Then in the testing file, `renderWithStore` can be used instead of `render` when needing access to redux

```tsx
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
// Add store imports
import myStore from 'store/myStore';

const rootReducer = combineReducers({
  myStore
  // ...and other needed stores
});

const renderWithStore = (
  ui,
  {
    initialState,
    store = createStore(
      rootReducer,
      initialState,
    ),
    ...renderOptions
  } = {},
) => {
  const Wrapper = ({ children }) => {
    return <Provider store={store}>{children}</Provider>;
  };

  return render(ui, { wrapper: Wrapper, ...renderOptions });
};
```

### Special case: Mock React Router

For react router, we need to wrap our test components in the Router and use `createMemoryHistory` from the ‘history’ library to mock the memory history.

```jsx
import { Router } from 'react-router-dom'; 
// Note that BrowserRouter ignores history prop, so need to use Router if need to use mockHistory
import { createMemoryHistory } from 'history';
import { render, fireEvent } from '@testing-library/react';

it('should navigate to my_page when the buton is clicked', () => {
  const mockHistory = createMemoryHistory({ initialEntries: ['/home'] });
  const { getByText } = render(
    <Router history={mockHistory}>
      <MyComponent />
    </Router>,
  );

  const button = getByText('Check out my page');

  fireEvent.click(button);
  expect(mockHistory.location.pathname).toMatch(/my_page/);
});
```

- If `useLocation` from react-router impacts the test, we need to mock it out as well, otherwise we would get error saying max depth call reached.
    
    ```jsx
    jest.mock('react-router', () => ({
      ...jest.requireActual('react-router'),
      useLocation: () => ({
        pathname: mockPath,
        key: `${mockPath}-mockKey`,
      }),
    }));
    ```
    

## Running the Tests & Debugging

- To run the tests, do `npm test` in the command line. This will enter watch mode and automatically run and rerun all new or modified tests.
    - You can then press `a` in the watch mode to run all tests, or `f` to run only failed tests.
    - To run a specific test file, put the file path name into your command line: `npm test src/components/MyComponent/MyComponent.test.js`
    - To run multiple specific test files, do `npm test testPath anotherTestPath`
        - If you’d like auto completing path name to work in the command line, instead of `npm test`, you’ll need to do `node_modules/.bin/jest`
    - To run a single test, do `.only` after `it`; to skip a test, do `x` in front of `it`
        
        ```tsx
        // Run only this test
        it.only('should do something', () => {});
        
        // Skip this test
        xit('should not do something', () => {});
        ```
        
- Use `debug` to visualize what the render method has created in the virtual DOM
    
    ```tsx
    it('should do something', () => {
    	const { debug, container } = render(<MyComponent />);
    	debug(); // will show the component in html format in the debugger log
    });
    ```
    
- Use vscode debugger to pause and start to inspect into a test
    - In the root directory, create `.vscode` folder, in there, create a `launch.json` file and put in the following code:
        
        ```tsx
        {
          "version": "0.2.0",
          "configurations": [
            {
              "name": "Run React tests",
              "type": "node",
              "request": "launch",
              "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
              "args": ["test", "--runInBand", "--no-cache", "--env=jsdom"],
              "cwd": "${workspaceRoot}",
              "protocol": "inspector",
              "console": "integratedTerminal",
              "internalConsoleOptions": "neverOpen"
            }
          ]
        }
        ```
        
    - And then can use the play button in the debugger panel to run the test. Running the tests this way will let us step into the breakpoints we add in the code. 

![vscode debugger screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yczxhhg7h7s6sh7ysf9l.png)
        
- Make sure providers are provided when needed — for libraries that consume context, for example styling libraries with a theming object, we should make sure their provider is imported and wrapped around our test components.
- If you are using Typescript and getting error that `data-testid` isn’t a defined prop, you can extend the html element prop the component is consuming so that `data-testid` is included. Example:
    
    ```tsx
    interface Props extends React.ComponentProps<'div'>{ // data-testid is an optional prop for the div elments 
      myPropOne: React.ReactNode;
      myPropTwo: string;
    }
    ```
    


That’s it for now. Hope this post covered some general questions you have when starting with React component testing. You might feel that there are so much to wrap your head around, don’t get intimidated, it will get easier once you get into the groove. Have fun testing!