DEV Community

Pearl Zeng-Yoders
Pearl Zeng-Yoders

Posted on

Get Started with React Component Testing, Mocking, and Debugging

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:

// 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
    });
}
Enter fullscreen mode Exit fullscreen mode

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:

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

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

        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

    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:

    const item = getByText(/Worked here for .*/);
    
    • We can also use Regex to assess the working years display is of a certain format:

      expect(item.innerHTML).toMatch(/^Worked here for .*years(?:(.*)months)?/gi);
      
  • To find an image, we can use -ByAltText, e.g.

    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:

    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.

    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

    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:

    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:

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();
});
Enter fullscreen mode Exit fullscreen mode

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:

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 });
Enter fullscreen mode Exit fullscreen mode

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.

    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

    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.

    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.

    // āŒ don't do this
    jest.mock('intersection');
    
    // āœ… do this
    jest.mock('lodash/intersection');
    
  • If mocking a library function that needs initialization:

    // āŒ 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
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 });
};
Enter fullscreen mode Exit fullscreen mode

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.

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/);
});
Enter fullscreen mode Exit fullscreen mode
  • 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.

    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

      // 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

    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:

      {
        "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

  • 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:

    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!

Top comments (0)