DEV Community

Assim EL HAMMOUTI
Assim EL HAMMOUTI

Posted on • Originally published at assim.me on

Testing React - Setup, Unit, Integration and E2E using Jest and Cypress (feat. GraphQL)

NOTE: This is a cross-post from my newsletter and Blog. I publish each email one week after it’s sent. Subscribe to get more content like this earlier right in your inbox! 💌

Hi ! This is a guide/cheatsheet that I comeback to read when I want to write tests for a project.

I thought this might help other fellow developers so here you go 😁

Setup

Install jest, cypress and helper libraries

yarn add jest @testing-library/react @testing-library/jest-dom -D
Enter fullscreen mode Exit fullscreen mode

Config

In this section we'll configure Jest and Cypress

Jest

Let's create a config file for Jest in the root directory:

module.exports = {
  // location.href will have this value
  testURL: 'https://example.com',
  // Add here folders to ignore
  testPathIgnorePatterns: ['/node_modules/'],
  setupTestFrameworkScriptFile: require.resolve(
    './test/setup.js'
  ),
  // path to components/modules to test
  modulePaths: ['<rootDir>/src'],
  moduleNameMapper: {
    // mock files that jest doesn't support like CSS and SVG files
    '\\.css$': '<rootDir>/test/module-mock.js',
    '\\.svg$': '<rootDir>/test/module-mock.js',
  },
  // collect coverage report from only the js files inside src
  collectCoverageFrom: ['**/src/**/*.js'],
  coverageThreshold: {
    global: {
      // 20 is just an example
      // you can change it to any value you want (below 100)
      statements: 20,
      branches: 20,
      functions: 20,
      lines: 20,
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Now create a test folder in the root directory and create setup.js file inside it:

// cleanup helper
import '@testing-library/react/cleanup-after-each'
// custom matchers for jest
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'
Enter fullscreen mode Exit fullscreen mode

also create a module-mock.js in the same test folder :

module.exports = {}
Enter fullscreen mode Exit fullscreen mode

Code coverage

In package.json add --coverage at the end of your test script:

{
  ...
  "scripts": {
    ...
    "test": "jest --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Watch mode

When coding, use Jest in watch mode to get instant feedback about the tests related to the files you are changing.
To use this feature, add a script to package.json and use it:

{
  ...
  "scripts": {
    ...
    "test:watch": "jest --watch"
  }
}
Enter fullscreen mode Exit fullscreen mode

Cypress

Install cypress and helpers:

yarn add cypress @testing-library/cypress -D
Enter fullscreen mode Exit fullscreen mode

then add a script to package.json to run cypress:

{
  ...
  "scripts": {
    ...
    "cy:open": "cypress open",
    "cy:run": "cypress run", // run all cypress tests
  }
}
Enter fullscreen mode Exit fullscreen mode
yarn cy:open
Enter fullscreen mode Exit fullscreen mode

Cypress records videos and takes screenshots of the app while running tests.
Let's add the folders that Cypress uses for this to .gitignore

  ...
  cypress/videos
  cypress/screenshots
Enter fullscreen mode Exit fullscreen mode

cypress.json

When running cypress open for the first time, it creates a bunch of files and folders inside a folder in the root dir called cypress. It also creates a file in the root dir called cypress.json. That's the configuration file cypress uses.

Let's add a baseUrl to use in our E2E test:

//cypress.json
{
  "baseUrl": "http://localhost:3000"
}
Enter fullscreen mode Exit fullscreen mode

@testing-library/cypress

@testing-library/cypress adds some very handy commands to cypress, let's configure it:

Go to <rootDir>/cypress/support, open index.js and add this line:

import '@testing-library/cypress/add-commands'
...
Enter fullscreen mode Exit fullscreen mode

Test utils (helpers):

Have a test-utils file that exports a set of tools that are used specifically for the project you are testing.

  • Example:

Export a render method that takes care of adding styled-components ThemeProvider HOC:

import React from 'react'
import {
  render as originalRender,
  wait,
} from '@testing-library/react'

const theme = {
  colors: {
    red: 'red',
  },
}

function render(component, renderOptions) {
  const utils = originalRender(
    <ThemeProvider theme={theme}>
      {component}
    </ThemeProvider>,
    renderOptions
  )
  return {
    ...utils,
  }
}
export { render }
Enter fullscreen mode Exit fullscreen mode

Now in your tests, import render from this test-utils file instead of @testing-library/react

Unit test

Write a unit test when you want to test the functionality of ONE function/component:

import React from 'react'
import { render } from '@testing-library/react'
import Paragraph from '../paragraph'

test('renders the text given', () => {
  const { getByText } = render(<Paragraph>Hello</Paragraph>)

  expect(getByText(/Hello/i)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

Integration test

Write an integration test when you want to test the functionality of several components working togather:

import React from 'react'
import { MockedProvider } from '@apollo/react-testing'
import wait from 'waait'
import { fireEvent } from '@testing-library/react'
import { render } from '../test-utils'
import App, { LOGIN_MUTATION } from '../app'

beforeEach(() => {
  window.localStorage.removeItem('token')
})

test('login as a user', async () => {
  const fakeUser = { id: 123, username: 'fakeuser' }
  const fakeUserCredentials = {
    ...fakeUser,
    password: 'stupidpassword123',
  }
  const token =
    'thisisjustanexampleofatoken-youcanuseafakedatageneratorinstead'
  const loginMutationMock = jest.fn()
  const loginMutationErrorMock = jest.fn()
  const mocks = [
    {
      request: {
        query: LOGIN_MUTATION,
        variables: {
          username: fakeUserCredentials.username,
          password: fakeUserCredentials.password,
        },
      },
      result: () => {
        loginMutationMock()
        return { data: { user: fakeUser, token: token } }
      },
      error: () => {
        loginMutationErrorMock()
      },
    },
  ]
  const { getByTestId, getByText, getByLabelText } = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <App />
    </MockedProvider>
  )
  // open login form dialog/modal
  fireEvent.click(getByText(/login/i))
  // fill out login form
  const usernameNode = getByLabelText(/username/i)
  const passwordNode = getByLabelText(/password/i)
  usernameNode.value = fakeUserCredentials.username
  passwordNode.value = fakeUserCredentials.password
  // submit login form
  fireEvent.click(getByText(/sign in/i))
  // wait for the mocked requests to finish
  await wait(0)
  // assert calls
  expect(loginMutationMock).toHaveBeenCalledTimes(1)
  expect(loginMutationErrorMock).not.toHaveBeenCalled()
  // assert login side-effect
  expect(window.localStorage.getItem('token')).toBe(token)
  expect(getByTestId('username').textContent).toEqual(
    fakeUser.username
  )
})
Enter fullscreen mode Exit fullscreen mode

End to end test:

Simplest definition : Imagine you've got a robot that obeys your commands, now ask it to test your app as a normal user 🤷‍♂️.

describe('authentication and registration', () => {
  let user

  beforeEach(() => {
    return cy
      .logout()
      .createNewUser()
      .then(u => (user = u))
      .visit('/')
  })

  it('register as a guest user', () => {
    const user = {
      username: 'user',
      email: 'hello@example.com',
      password: 'password123',
    }
    cy.getByText(/register/i)
      .click()
      .getByLabelText(/username/i)
      .type(user.username)
      .getByLabelText(/email/i)
      .type(user.email)
      .getByLabelText(/password/i)
      .type(user.password)
      .getByText(/register/i)
      .click()
      .assertRoute('/')
    cy.getByTestId('username').should(
      'contain',
      user.username
    )
  })

  it('login as a user', () => {
    cy.getByText(/login/i)
      .click()
      .getByLabelText(/username/i)
      .type(user.username)
      .getByLabelText(/password/i)
      .type(user.password)
      .getByText(/sign in/i)
      .click()
      .assertRoute('/')
    cy.getByTestId('username').should(
      'contain',
      user.username
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

I'll try to improve this post and add to it but please feel free to send a PR in case you want to correct/add/edit something ❤️

Top comments (0)