Dwarves
Memo
Type ESC to close search bar

Different Ways To Test React Application

Introduction

There are 2 practical types of testing a front end application: test its implementation details (unit test, integration test combine with assert states of the component being tested) or testing its behavior base on perspective (e2e, integration test).

The content below assumes you already know about React hook (which is a new feature of React 16.08)

This is a repository link: https://github.com/phmngocnghia/TestReactComponent This repository contains source code for the carousel component which we gonna test.

Unit testing using component react-testing-library

The react-testing-library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices.

The react-hooks-testing-library allows you to create a simple test harness for React hooks that handles running them within the body of a function component, as well as providing various useful utility functions for updating the inputs and retrieving the outputs of your amazing custom hook.

To run integration tests for the application in the repository above, you can run the command:

yarn test — This command will execute all unit test for the carousel component.

To test React component using react-testing-utility, we mount it with the render function exposed by the library @testing-library/react. Then we use object spread notation to extract utility functions such as getById, getByText… etc.

Their purpose is used for select element purpose. The return value would be a dom element. You can view all selector function from render function at https://testing-library.com/docs/dom-testing-library/api-queries

When you’re writing tests, you often need to check that values meet certain conditions. assert function gives you access to an API that let you validate different things. the API of an assertion function depends on what kind of assert library your test run on. The example below used Jest as both test runner and assertion library, more details at https://jestjs.io/docs/en/getting-started

import { render, cleanup } from '@testing-library/react'

it('arrow left handler event set currentIndex (initialised) from 1 to 2 when invoked', () => {
  const {
    getByTestId
  } = render(<App />)

  const rightArrowButton = getByTestId('rightCarouselArrowButton')
  rightArrowButton.click()

  expect(global.exposedCarouselComponentData.currentImageIndex).toBe(1)
})

Different when compare to enzyme, we can’t assert and access states of the function component when testing. We exposed it variable outside to global object and assert when testing:

// Expose data out for testing process
  if (process.env.REACT_APP_IS_TEST_MODE === 'true') {
    global.exposedCarouselComponentData = {
      arrowLeftOnClick,
      arrowRightClick,
      currentImageIndex
    }
  }

Pros: Blazing fast

Cons:

Best use for: Test implement detail while developing. Assert state by exposing the state to the global object

Testing react using react-testing-hook-library

Hook function not only allows us to use features of the class component inside the functional component, extract and reuse many repetitive logic patterns but also allow us to extract the logic belong specific domain of that component to an isolated part.

const Carousel = ({
  currentFocusImageIndex,
  onChangeNext,
  onChangePrev,
  children = [],
}) => {
  const containerRef = useRef()

  const { widthOfInnerContainer, containerWidth } = useCarouselReactResizeObserver({
    containerRef,
    childrenLength: children.length
  })

  const {
    setCurrentImageIndex,
    currentImageIndex,
  } = useCarouseSetImageIndex({
    childrenLength: children.length,
    displayPaginator: 10,
  })

  // Handle dnd
  const arrowRightClick = () => {
    // From the item in last index move backward 1 item
    if (currentImageIndex === children.length-1) {
      setCurrentImageIndex(0)
      return
    }

    setCurrentImageIndex(currentImageIndex + 1)
  }

  const arrowLeftOnClick = () => {
    // From the item index 0 move backward 1 item
    if (currentImageIndex === 0) {
      setCurrentImageIndex(children.length - 1)
      return
    }

    setCurrentImageIndex(currentImageIndex-1)
  }

  // Expose data out for testing process
  if (process.env.REACT_APP_IS_TEST_MODE === 'true') {
    global.exposedCarouselComponentData = {
      arrowLeftOnClick,
      arrowRightClick,
      currentImageIndex
    }
  }

  return (
    // Copy div from outter container and watch for size change
    <div className={innerContainer} ref={containerRef}>
      {/* Arrow */}
      <button data-testid="leftCarouselArrowButton" className={arrowButton} onClick={arrowLeftOnClick}>
        <FaChevronLeft size="25" color="white" />
      </button>
      <button data-testid="rightCarouselArrowButton" className={[arrowButton, arrowButtonRight].join(' ')} onClick={arrowRightClick}>
        <FaChevronRight size="25" color="white" />
      </button>

      {/* Use for transit slide */}
      <div
        data-testid="carouselContainer"
        className={container}
        style={{
          width: widthOfInnerContainer,
          transform: `translateX(-${currentImageIndex * containerWidth}px)`
        }}>
        {/* slide container */}
        {children.map(element => (
          <div
            data-testid="CarouselChildContainer"
            className={childContainer}
            style={{
              width: containerWidth
            }}
          >
            {element}
          </div>
        ))}
      </div>
    </div>
  )
}

In the code above, the logic of the hook may only use apply to carousel-component and may not apply to other components but it solves a specific problem of the carousel component so I extract it into a hook.

import {
  useState,
  useEffect,
  useMemo, 
} from 'react'

export default ({
  childrenLength,
  initialImageIndex = 0,
  numDisplayPaginator,
}) => {
  const [currentImageIndex, setCurrentImageIndex] = useState(0)

  useEffect(() => {
    setCurrentImageIndex(initialImageIndex)
  }, [initialImageIndex])

  const endOfTheLeftMiddle = useMemo(() => {
    return (numDisplayPaginator / 2) + 1
  }, [numDisplayPaginator])

  const minDisplayPaginatorIndex = useMemo(() => {
    // * Case 1: index in 1 -> (numDisplayPaginator/ 2)
    if (1 >= currentImageIndex && currentImageIndex <= endOfTheLeftMiddle) {
      return 0
    }

    // * Case 2: index + 1 > (numDisplayPaginator / 2)
    let offset = currentImageIndex - endOfTheLeftMiddle
    let maxDisplayPaginatorIndex = offset + numDisplayPaginator

    if (maxDisplayPaginatorIndex >= childrenLength) {
      offset -= (maxDisplayPaginatorIndex - childrenLength)
    }

    return 1 + offset
  }, [childrenLength, currentImageIndex, endOfTheLeftMiddle, numDisplayPaginator])

  const maxDisplayPaginatorIndex = useMemo(() => {
    // * Case 1: index in 1 -> (numDisplayPaginator/ 2)
    if (1 >= currentImageIndex && currentImageIndex <= (numDisplayPaginator)) {
      return numDisplayPaginator
    }

    // * Case 2: index + 1 > (numDisplayPaginator / 2)
    const offset = currentImageIndex - endOfTheLeftMiddle
    let maxDisplayPaginatorIndex = offset + numDisplayPaginator

    if (maxDisplayPaginatorIndex >= childrenLength) {
      maxDisplayPaginatorIndex = childrenLength
    }

    return maxDisplayPaginatorIndex
  }, [childrenLength, currentImageIndex, endOfTheLeftMiddle, numDisplayPaginator])

  return {
    currentImageIndex,
    setCurrentImageIndex,
    maxDisplayPaginatorIndex,
    minDisplayPaginatorIndex,

    // Testing purpose
    endOfTheLeftMiddle
  }
}

Test a hook is very identical when we test against react component. We using the render hook function to mount the hook. the result of the function would be the instance of the hook. Where you can access all property of the hooks such as Its states or its function as long as you return the data you want to access from the hook.

import { renderHook, act } from 'react-hooks-testing-library'
import useCarouseImageIndex from '../../components/Carousel/useCarouseSetImageIndex'

  it('return correct end of the left middle of display paginator number', () => { const { result } = renderHook(() => useCarouseImageIndex({
      childrenLength: 100,
      initialImageIndex: 0,
      numDisplayPaginator: 10
    }))

    expect(result.current.endOfTheLeftMiddle).toBe(6)
  })

Pros:

Cons: Can only use for test hook of functional react component.

Best use for: Test implement detail while developing.

E2e test using Cypress

End-to-end testing is awesome because it mirrors the user’s experience. Where you might need a ton of unit tests to get good coverage (the kind where you test that a function returns a value you expect).

E2e test using cypress is executed by cypress test runner. With cy instance, Its exposed some utility functions such as visit page, query selector on that page, assert if the selector existed and many more.

it('Render container of child cotainer equal the width of wrapper container (that wrap carousel component)', async () => {
      cy.visit('http://localhost:3000')
      cy.viewport(1920, 1080)
      cy.wait(200)
      cy.getByTestId('testCarouselContainer').then(elem => {
        const carouselContainerWidth = elem[0].clientWidth
        cy.getAllByTestId('testCarouselContainer').then(carouselChildContainers => {
          for (let carouselChildContainer of carouselChildContainers) {
            expect(carouselChildContainer.clientWidth).to.be.equal(carouselContainerWidth)
          }
        })
      })
    })
import promisify from 'cypress-promise'

   it('Render container of child cotainer equal the width of wrapper container (that wrap carousel component) - Resize version', async () => {
      cy.visit('http://localhost:3000')
      cy.viewport(1920, 1080)
      // Give obersve resizer some time for react
      cy.wait(100)
      cy.viewport(800, 1600)
      cy.wait(250)

      const elem = await promisify(cy.getByTestId('testCarouselContainer'))
      const carouselContainerWidth = elem[0].clientWidth
      cy.getAllByTestId('testCarouselContainer').then(carouselChildContainers => {
        for (let carouselChildContainer of carouselChildContainers) {
          expect(carouselChildContainer.clientWidth).to.be.equal(carouselContainerWidth)
          expect(carouselChildContainer.clientWidth).to.be.equal(800)
        }
      })
    })

You can view Its full API detail at https://docs.cypress.io/api/api/table-of-contents.html To run e2e tests for the application in the repository above, you can run the command:

Pros:

Cons:

Best use for:

Conclusion

I’m writing this mainly as a reference for myself, but hopefully, other people will find this useful as well.

Reference links: