ui-testing-best-practices

Unit Testing React components with Cypress

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 Storybook 6.2 release!


Now that unit testing React component is possible with Cypress, this is an extending chapter of the Testing a component with Cypress and Storybook one.

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

The motivations were pretty simple:

And the approach was simple too:

But there were some caveats

Some of the problems of my experiment could be mitigated by daedalius 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 chapter. 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:

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,
            },
          },
        ],
      },
    ],
  },
}
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "types": ["cypress", "cypress-wait-until"]
  }
}

please note that:

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

{
  "experimentalComponentTesting": true,
  "componentFolder": "cypress/component"
}

are enough to start playing with a cypress/component/VirtualList.spec.tsx test! From the previous chapter, 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 '[@testing](http://twitter.com/testing)-library/cypress/add-commands'

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')
  })
})

Compared to the Storybook-related chapter:

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

at the beginning are needed to let VSCode correctly leverage TypeScript suggestions and error reporting

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

Callback Testing

Porting all the tests from the previous chapter 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](http://twitter.com/handleSelect)').should((stub) => {
      expect(stub).to.have.been.calledOnce
      expect(stub).to.have.been.calledWith([testItems[0].id])
    })
  })
})

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

Conclusions

Take a look at the result in this video. 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.



Crossposted by NoriSte on dev.to and Medium.