DEV Community

Cover image for Know this easily test React app
Phan Công Thắng
Phan Công Thắng

Posted on • Originally published at thangphan.xyz

Know this easily test React app

Jest and Testing Library were the most powerful tool for testing React App. In this post, we are going to discover the important concept of them.

Let's dig in!

This is the simplest test that we can write in the first time using Jest.

test('1 plus 2 equal 3', () => {
  expect(1 + 2).toBe(3)
})
Enter fullscreen mode Exit fullscreen mode

Test Asynchronous

Suppose that I have a fake API that returns the user response with id: 1, in the test case, I intentionally set change id: 3 to check whether the test works properly or not, and I end up with a passed message.

The reason is that the test case is completed before the promise finishes.

test('user is equal user in response', () => {
  const user = {
    userId: 1,
    id: 3,
    title: 'delectus aut autem',
    completed: false,
  }

  fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then((response) => response.json())
    .then((json) => expect(user).toEqual(json))
})
Enter fullscreen mode Exit fullscreen mode

In order to avoid this bug, we need to have return in front of fetch.

test('user is equal user in response', () => {
  const user = {
    userId: 1,
    id: 3,
    title: 'delectus aut autem',
    completed: false,
  }

  return fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then((response) => response.json())
    .then((json) => expect(user).toEqual(json))
})
Enter fullscreen mode Exit fullscreen mode

The test case above can rewrite using async, await:

test('user is equal user in response using async, await', async () => {
  const user = {
    userId: 1,
    id: 2,
    title: 'delectus aut autem',
    completed: false,
  }

  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const resJson = await res.json()

  expect(user).toEqual(resJson)
})
Enter fullscreen mode Exit fullscreen mode

Useful methods

beforeAll: To add some code that we want to run once before the test cases is run.

afterAll: To add some code that we want to run after all test cases are finished. e.g. clear the database.

beforeEach: To add some code that we want to run before each test case.

afterEach: To add some code that we want to run at the point that each test case finishes.

Suppose that I have three test cases, and I set:

beforeEach(() => {
  console.log('beforeEach is working...')
})
Enter fullscreen mode Exit fullscreen mode

Three console will appear on my terminal. Conversely, Using beforeAll I only see one console.

The logic way is the same with afterEach and afterAll.

The order run

We already have describe(combines many test cases), test(test case).

What is the order that jest run if test file was mixed by many describe, test?

You only need to remember this order: describe -> test.

To illustrate:

describe('describe for demo', () => {
  console.log('this is describe')

  test('1 plus 2 equal 3', () => {
    console.log('this is test case in describe')

    expect(1 + 2).toBe(3)
  })

  describe('sub-describe for demo', () => {
    console.log('this is sub-describe')

    test('2 plus 2 equal 4', () => {
      console.log('this is test case in sub-describe')

      expect(2 + 2).toBe(4)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Can you spot on the order in the example above?

My terminal log:

  • this is describe

  • this is sub-describe

  • this is test case in describe

  • this is test case in sub-describe

Mock function

I think the most powerful of Jest is having a mock function that we are able to mock the params, object which defined by the new keyword, and customize the return value.

This is an example:

function plusTwoNumbers(
  list: Array<number>,
  callback: (a: number, b: number) => void,
) {
  callback(list[0], list[1])
}

test('mock function callback', () => {
  const mockFnc = jest.fn((a, b) => console.log('total:', a + b))

  plusTwoNumbers([1, 2], mockFnc)
})
Enter fullscreen mode Exit fullscreen mode

We mock callback function, get the params of it, and customize the result console.log("total:", a + b).

We are also able to mock modules, e.g. I use uuid in order to generate a unique id.

When I move on to testing, instead of using uuid, I can mock the uuid module like the code below:

Normally, whenever I call uuid.v4() I will get a random value like this: 5442486-0878-440c-9db1-a7006c25a39f

But I want my value to be 1234, I can use the code below:

import * as uuid from 'uuid'

jest.mock('uuid')

test('mock uuid module', () => {
  uuid.v4.mockReturnValue('1234')

  console.log('uuid.v4()', uuid.v4())

  // 1234
})
Enter fullscreen mode Exit fullscreen mode

Otherwise, I can use mockImplementation to customize.

uuid.v4.mockImplementation(() => '1234')
Enter fullscreen mode Exit fullscreen mode

mockImplementation is the function that we customize the function that is created from other modules.

Config Jest

I'm going to introduce to you about the most important configs in Jest.

Let's go!

  • collectCoverageFrom

This config helps Jest knows exactly the place that needs to collect information, and check coverage. It is very useful, you can run:

Run jest --coverage in order to figure out the component, the function, we still need to write test, and discover the spots we still don't test yet.

  • moduleDirectories

This config points to the module that we will use in the test file.

By default, it was configured ["node_modules"], and we are able to use the the module under node_modules folder in our test cases.

  • moduleNameMapper

This config provides for us the ability to access the resources, based on the place that we have set.


moduleNameMapper: {
  "assets/(*)": [
    "<rootDir>/images/$1"
  ]
}
Enter fullscreen mode Exit fullscreen mode

See the example above, now we set the path assets/(*) that pointed to <rootDir>/images/$1.

If I set assets/logo.png, Jest will find <rootDir>/images/logo.png.

  • rootDir

By default, it is the place that contains jest.config.js, package.json.

The place is where Jest will find to use modules, and run test cases.

It turns out I can set "rootDir: 'test'" and run test cases without config roots, but I shouldn't do this.

  • roots

This is the config that we set the place that test files belong to.

For example:

If I set:

roots: ['pages/']
Enter fullscreen mode Exit fullscreen mode

but I write test in __test__ folder which is the same level with pages/. No test cases will be run with the config above. I need to change pages/ -> __test__.

  • testMatch

We use this config in order to communicate to Jest what files we want to test, otherwise, please skip!

  • testPathIgnorePatterns

Please ignore files under a place, that is the reason this config exists.

  • transform

Sometimes, in our test cases, we write some new code that node doesn't support at all, so we need to transform to the code that Jest can understand.

If my project use typescript, I need to set up transform in order to make typescript to javascript code that node can understand.

  • transformIgnorePatterns

We might have some files, some folders we don't want to transform, so we use this config.

How to write test

We need to write tests in order to be more confident about the code that we wrote. So when we think about the test cases, the core concept is we have to think about the use case, do not think about the code. It means we must focus
into what's the future that the code can support for users.

This is the main concept when we think about creating test cases.

e.g:

I have created a react-hook in order to support four features below:

  1. returns the value in first data using first property, condition true.

  2. returns the value in second data using second property, condition false.

  3. returns the value in second data using first property, condition false.

  4. returns the default value with second data undefined, condition false.

import * as React from 'react'

type Props<F, S> = {
  condition: boolean
  data: [F, S]
}

function useInitialState<F, S>({condition, data}: Props<F, S>) {
  const giveMeState = React.useCallback(
    (
      property: keyof F,
      anotherProperty: S extends undefined ? undefined : keyof S | undefined,
      defaultValue: Array<string> | string | number | undefined,
    ) => {
      return condition
        ? data[0][property]
        : data[1]?.[anotherProperty ?? (property as unknown as keyof S)] ??
            defaultValue
    },

    [condition, data],
  )

  return {giveMeState}
}

export {useInitialState}
Enter fullscreen mode Exit fullscreen mode

So I only need to write four test cases for the four features above:

import {useInitialState} from '@/utils/hooks/initial-state'

import {renderHook} from '@testing-library/react-hooks'

describe('useInitialState', () => {
  const mockFirstData = {
    name: 'Thang',
    age: '18',
  }

  test('returns the value in first data using first property, condition true', () => {
    const mockSecondData = {
      name: 'Phan',
      age: 20,
    }

    const {result} = renderHook(() =>
      useInitialState({
        condition: Boolean(mockFirstData),
        data: [mockFirstData, mockSecondData],
      }),
    )

    const data = result.current.giveMeState('name', undefined, '')

    expect(data).toBe(mockFirstData.name)
  })

  test('returns the value in second data using second property, condition false', () => {
    const mockSecondData = {
      firstName: 'Phan',
      age: 20,
    }

    const {result} = renderHook(() =>
      useInitialState({
        condition: Boolean(false),
        data: [mockFirstData, mockSecondData],
      }),
    )

    const data = result.current.giveMeState('name', 'firstName', '')

    expect(data).toBe(mockSecondData.firstName)
  })

  test('returns the value in second data using first property, condition false', () => {
    const mockSecondData = {
      name: 'Phan',
      age: 20,
    }

    const {result} = renderHook(() =>
      useInitialState({
        condition: Boolean(false),
        data: [mockFirstData, mockSecondData],
      }),
    )

    const data = result.current.giveMeState('name', undefined, '')

    expect(data).toBe(mockSecondData.name)
  })

  test('returns the default value with second data undefined, condition false', () => {
    const mockDefaultValue = 21

    const {result} = renderHook(() =>
      useInitialState({
        condition: Boolean(false),
        data: [mockFirstData, undefined],
      }),
    )

    const data = result.current.giveMeState('age', undefined, mockDefaultValue)

    expect(data).toBe(mockDefaultValue)
  })
})
Enter fullscreen mode Exit fullscreen mode

Testing Library

Let's take a slight review about the main things in Testing Library.

  • getBy..: we find the DOM element, throw error if no element is found.
  • queryBy..: we find the DOM element, return null if no element is found.
  • findBy..: we find the DOM element, throw an error if no element is found, the search process is a promise.

The list below is the priority we should use in order to write test nearer with the way that our app is used.

  • getByRole

  • getByLabelText

  • getByAltText

  • getByDisplayValue

For example:

I have a component that contains two components: AutoAddress, Address.I need to find the use case that I want to support in order to create test cases.

This is a test case: by default, name value of inputs was set.

  1. render the components

  2. create the mockResult value

  3. add assertions

test('by default, name of address input was set', async () => {
  render(
    <AutoAddress wasSubmitted={false}>
      <Address wasSubmitted={false} />
    </AutoAddress>,
  )

  const mockResult = {
    namePrefectureSv: 'prefertureSv',
    namePrefectureSvLabel: 'prefectureSvLabel',
    nameCity: 'city',
  }

  expect(screen.getByLabelText('Prefecture Code')).toHaveAttribute(
    'name',
    mockResult.namePrefectureSv,
  )

  expect(screen.getByLabelText('Prefecture')).toHaveAttribute(
    'name',
    mockResult.namePrefectureSvLabel,
  )

  expect(screen.getByLabelText('City')).toHaveAttribute(
    'name',
    mockResult.nameCity,
  )
})
Enter fullscreen mode Exit fullscreen mode

And this is a test case: returns one address through postCode.

  1. render the components

  2. create the mockResult value

  3. mock the request API

  4. input the postCode

  5. click the search button

  6. add assertions

test('returns one address through postCode', async () => {
  const mockResult = [
    {
      id: '14109',
      zipCode: '1880011',
      prefectureCode: '13',
      city: 'Tokyo',
    },
  ]

  server.use(
    rest.get(
      `${process.env.NEXT_PUBLIC_API_OFF_KINTO}/${API_ADDRESS}`,
      (req, res, ctx) => {
        return res(ctx.json(mockResult))
      },
    ),
  )

  render(
    <AutoAddress wasSubmitted={false}>
      <Address wasSubmitted={false} />
    </AutoAddress>,
  )

  // input the post code value

  userEvent.type(screen.getByLabelText('first postCode'), '111')
  userEvent.type(screen.getByLabelText('second postCode'), '1111')

  // search the address

  userEvent.click(screen.getByRole('button', {name: /search address/i}))

  // wait for the search process finishes.

  await waitForElementToBeRemoved(() =>
    screen.getByRole('button', {name: /searching/i}),
  )

  const address = mockResult[0]
  const {prefectureCode, city} = address

  expect(screen.getByLabelText('Prefecture Code')).toHaveAttribute(
    'value',
    prefectureCode,
  )

  expect(screen.getByLabelText('Prefecture')).toHaveAttribute(
    'value',
    PREFECTURE_CODE[prefectureCode as keyof typeof PREFECTURE_CODE],
  )

  expect(screen.getByLabelText('City')).toHaveAttribute('value', city)
})
Enter fullscreen mode Exit fullscreen mode

Recap

We just learned the main concepts in Testing React App! Let's recap some key points.

  • Testing asynchronous need to have return in front of promise.
  • We are able to control testing using Jest configs.
  • Thinking test cases, we must forget about code, focus on the use case.
  • The order of DOM methods in Testing Library.

Top comments (0)