DEV Community

Cover image for Unit Testing React components with Cypress
Stefano Magni
Stefano Magni

Posted on • Edited on

Unit Testing React components with Cypress

Cypress 4.5.0 is out with a long-awaited feature: first-class support for framework’s components render.

I’m working on a big UI Testing Best Practices project on GitHub, I share this post to spread it and have direct feedback.


UPDATE: Cypress 10 is out with Component Testing integrated with E2E testing, please check it out and ignore all the configuration steps reported below since they are outdated!


UPDATE: Cypress 7 is out with a brand-new Component Test support, check it out! And other exciting news is on the way thanks to the Storybook 6.2 release!


Two months ago, I wrote the “Testing a Virtual List component with Cypress and Storybook” article. This is an extending article now that unit testing React component is possible with Cypress.

The goal of the previous article was to run some experiments in the React Component Testing world, a really important topic nowadays.

The motivations were pretty simple:

  • you probably already have Storybook in action in your team (if not, consider adding it!)

  • you could be not familiar with testing components with Testing Library or you could be biased about JSDom or you could want to test your UI components in a real browser, not in a simulated DOM environment

  • you could be familiar with Cypress or TestCafé (if not, consider them for your UI tests) and you could want to use just a single tool for your tests

And the approach was simple too:

  • exposing the story’ props to the testing tool, used to control the rendered component

  • pick up them from Cypress/TestCafé, automating user actions and asserting about the contents of the props

But there were some caveats

  • performance: in the article, I put some extra-efforts to minimize the impact of story switching slowness

  • testing and stories coupling: since Storybook is consumed even by Cypress, stories are going to be accountable not only for sharing the design system across the team but for the component tests too

  • callback testing got tough: checking the params and the calls of the callback props is difficult

Some of the problems of my experiment could be mitigated by Dmitriy Tishin approach but the solution is not optimal yet, but then…

Cypress 4.5.0 has been released

On April, 28th, Cypress 4.5.0 has been released, the only released feature is the following

Cypress now supports the execution of component tests using framework-specific adaptors when setting the experimentalComponentTesting configuration option to true. For more details see the cypress-react-unit-test and cypress-vue-unit-test repos.

What does it mean? That Cypress can now directly mount a React component giving the cypress-react-unit-test a new birth! Before Cypress 4.5.0 release, the plugin was pretty limited but now it has first-class support! In fact, the cypress-react-unit-test is now rock-solid and a meaningful plugin.

Testing the VirtualList component: second episode

The component is always the same, the VirtualList, read more about it in the previous article. We need to set up both the cypress-react-unit-test and the TypeScript conversion (the component is written in TypeScript, it is part of a Lerna monorepo, and it is compiled with Webpack). Both the steps are straightforward but if the plugin has an installation-dedicated section in its documentation, the TypeScript compilation could not be obvious because there are, outdated or partial, a lot of different approaches and resources.
The most concise yet effective solution is André Pena’s one, so all I had to do is:

  • adding a cypress/webpack.config.js file
module.exports = {
  mode: 'development',
  devtool: false,
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: 'ts-loader',
            options: {
              // skip typechecking for speed
              transpileOnly: true,
            },
          },
        ],
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode
  • adding a cypress/tsconfig.json file
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "types": ["cypress", "cypress-wait-until"]
  }
}
Enter fullscreen mode Exit fullscreen mode

please note that:

  • the ../tsconfig.json file is the same used by the React app

  • cypress-wait-until is not mandatory but I use it a lot and it is one of the most installed plugins for Cypress

The above transpiling-related files, along with the following cypress.json file

{
  "experimentalComponentTesting": true,
  "componentFolder": "cypress/component"
}
Enter fullscreen mode Exit fullscreen mode

are enough to start playing with a cypress/component/VirtualList.spec.tsx test! From the previous article, the first test was the standard rendering, the “When the component receives 10000 items, then only the minimum number of items are rendered” test, et voilà:

/// <reference types="Cypress" />
/// <reference types="cypress-wait-until" />

import React from 'react'
import { mount } from 'cypress-react-unit-test'

import { VirtualList } from '../../src/atoms/VirtualList'
import { getStoryItems } from '../../stories/atoms/VirtualList/utils'

describe('VirtualList', () => {
  it('When the list receives 10000 items, then only the minimum number of them are rendered', () => {
    // Arrange
    const itemsAmount = 10000
    const itemHeight = 30
    const listHeight = 300
    const items = getStoryItems({ amount: itemsAmount })
    const visibleItemsAmount = listHeight / itemHeight

    // Act
    mount(
      <VirtualList
        items={items}
        getItemHeights={() => itemHeight}
        RenderItem={createRenderItem({ height: itemHeight })}
        listHeight={listHeight}
      />,
    )

    // Assert
    const visibleItems = items.slice(0, visibleItemsAmount - 1)
    itemsShouldBeVisible(visibleItems)

    // first not-rendered item check
    cy.findByText(getItemText(items[visibleItemsAmount]))
      .should('not.exist')
  })
})
Enter fullscreen mode Exit fullscreen mode

Compared to the Storybook-related article:

  • the
/// <reference types="Cypress" />
/// <reference types="cypress-wait-until" />
Enter fullscreen mode Exit fullscreen mode

at the beginning are needed to let VSCode correctly leverage TypeScript suggestions and error reporting (it works for plain JavaScript files too)

  • we use cypress-react-unit-test’ mount API to mount the component, nothing especially new if you are used to the Testing Library APIs

Nothing more, the Cypress test continues the same as the Storybook-related one 😊

Callback Testing

Porting all the tests from the previous article is quite easy, what was missing is the callback testing part of the “selection test”.

Creating a WithSelectionManagement wrapper component that renders the VirtualList one and manages items selection is quite easy and we can pass it our stub and assert about it

it('When the items are clicked, then they are selected', () => {
  const itemHeight = 30
  const listHeight = 300
  let testItems

  const WithSelectionManagement: React.FC<{
    testHandleSelect: (newSelectedIds: ItemId[]) => {}
  }> = props => {
    const { testHandleSelect } = props
    const items = getStoryItems({ amount: 10000 })

    const [selectedItems, setSelectedItems] = React.useState<(string | number)[]>([])

    const handleSelect = React.useCallback<(params: OnSelectCallbackParams<StoryItem>) => void>(
      ({ newSelectedIds }) => {
        setSelectedItems(newSelectedIds)
        testHandleSelect(newSelectedIds)
      },
      [setSelectedItems, testHandleSelect],
    )

    React.useEffect(() => {
      testItems = items
    }, [items])

    return (
      <VirtualList
        items={items}
        getItemHeights={() => itemHeight}
        listHeight={listHeight}
        RenderItem={createSelectableRenderItem({ height: itemHeight })}
        selectedItemIds={selectedItems}
        onSelect={handleSelect}
      />
    )
  }
  WithSelectionManagement.displayName = 'WithSelectionManagement'

  mount(<WithSelectionManagement testHandleSelect={cy.stub().as('handleSelect')} />)

  cy.then(() => expect(testItems).to.have.length.greaterThan(0))
  cy.wrap(testItems).then(() => {
    cy.findByText(getItemText(testItems[0])).click()
    cy.get('@handleSelect').should(stub => {
      expect(stub).to.have.been.calledOnce
      expect(stub).to.have.been.calledWith([testItems[0].id])
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Please refer to the full SinonJS (wrapped and used by Cypress) Stub/Spy documentation for the full APIs.

Conclusions

Here a screenshot of the last test, the most complete one

The test runner reporting the test steps on the left and the rendered VirtualList on the right.The last test with the stub checks.

and this is the recording of all the tests

The test lasts now less than seven seconds, without depending nor loading Storybook, leveraging first-class Cypress support.

What’s next? The cypress-react-unit-test plugin is quite stable and useful now, a whole new world of experiments is open and a lot of small-to-medium projects could choose to leverage Cypress as a single testing tool. I’m waiting for your comments and experience 😊

Related articles

Other articles of mine you would find interesting:

Top comments (2)

Collapse
 
bmitchinson profile image
Ben Mitchinson

Of course the day I need to upgrade my react cypress testing suite to 4.5, you've updated this guide with the April updates. Thank you so so much! Will post with any difficulties / findings that others might find helpful as well.

We'll have to see how coverage plays in, I've had so much trouble getting interaction + unit tests to both contribute to one cohesive report.

Collapse
 
noriste profile image
Stefano Magni

Thanks, waiting for your findings, Ben 😊