[React Testing] Write React Application Integration Tests with React Testing Library

You can get a huge amount of confidence and coverage from integration tests that test an entire page, or even your entire app. Let’s write a test that renders our whole app using React Testing Library and navigate around it like a normal user would. These tests are typically a bit longer, but they provide a huge amount of value.

 

 App.js
import React from ‘react‘
import { BrowserRouter as Router, Route, Link, Switch } from ‘react-router-dom‘
import { submitForm } from ‘./api‘

const MultiPageForm = React.createContext()

function MultiPageFormProvider({ initialValues = {}, ...props }) {
  const [initState] = React.useState(initialValues)
  const [form, setFormValues] = React.useReducer(
    (s, a) => ({ ...s, ...a }),
    initState,
  )
  const resetForm = () => setFormValues(initialValues)
  return (
    <MultiPageForm.Provider
      value={{ form, setFormValues, resetForm }}
      {...props}
    />
  )
}

function useMultiPageForm() {
  const context = React.useContext(MultiPageForm)
  if (!context) {
    throw new Error(
      ‘useMultiPageForm must be used within a MiltiPageFormProvider‘,
    )
  }
  return context
}

function Main() {
  return (
    <>
      <h1>Welcome home</h1>
      <Link to="/page-1">Fill out the form</Link>
    </>
  )
}

function Page1({ history }) {
  const { form, setFormValues } = useMultiPageForm()
  return (
    <>
      <h2>Page 1</h2>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          history.push(‘/page-2‘)
        }}
      >
        <label htmlFor="food">Favorite Food</label>
        <input
          id="food"
          value={form.food}
          onChange={(e) => setFormValues({ food: e.target.value })}
        />
      </form>
      <Link to="/">Go Home</Link> | <Link to="/page-2">Next</Link>
    </>
  )
}

function Page2({ history }) {
  const { form, setFormValues } = useMultiPageForm()
  return (
    <>
      <h2>Page 2</h2>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          history.push(‘/confirm‘)
        }}
      >
        <label htmlFor="drink">Favorite Drink</label>
        <input
          id="drink"
          value={form.drink}
          onChange={(e) => setFormValues({ drink: e.target.value })}
        />
      </form>
      <Link to="/page-1">Go Back</Link> | <Link to="/confirm">Review</Link>
    </>
  )
}

function Confirm({ history }) {
  const { form, resetForm } = useMultiPageForm()
  function handleConfirmClick() {
    submitForm(form).then(
      () => {
        resetForm()
        history.push(‘/success‘)
      },
      (error) => {
        history.push(‘/error‘, { state: { error } })
      },
    )
  }
  return (
    <>
      <h2>Confirm</h2>
      <div>
        <strong>Please confirm your choices</strong>
      </div>
      <div>
        <strong id="food-label">Favorite Food</strong>:{‘ ‘}
        <span aria-labelledby="food-label">{form.food}</span>
      </div>
      <div>
        <strong id="drink-label">Favorite Drink</strong>:{‘ ‘}
        <span aria-labelledby="drink-label">{form.drink}</span>
      </div>
      <Link to="/page-2">Go Back</Link> |{‘ ‘}
      <button onClick={handleConfirmClick}>Confirm</button>
    </>
  )
}

function Success() {
  return (
    <>
      <h2>Congrats. You did it.</h2>
      <div>
        <Link to="/">Go home</Link>
      </div>
    </>
  )
}

function Error({
  location: {
    state: { error },
  },
}) {
  return (
    <>
      <div>Oh no. There was an error.</div>
      <pre>{error.message}</pre>
      <Link to="/">Go Home</Link>
      <Link to="/confirm">Try again</Link>
    </>
  )
}

function App() {
  return (
    <MultiPageFormProvider initialValues={{ food: ‘‘, drink: ‘‘ }}>
      <Router>
        <Switch>
          <Route path="/page-1" component={Page1} />
          <Route path="/page-2" component={Page2} />
          <Route path="/confirm" component={Confirm} />
          <Route path="/success" component={Success} />
          <Route path="/error" component={Error} />
          <Route component={Main} />
        </Switch>
      </Router>
    </MultiPageFormProvider>
  )
}

export default App

  

Test:

import React from react
import { render, fireEvent } from @testing-library/react
import { submitForm as mockSubmitForm } from ../extra/api
import App from ../extra/app
import @testing-library/jest-dom/extend-expect

jest.mock(../extra/api)

test(Can fill out a form across multiple pages, async () => {
  mockSubmitForm.mockResolvedValueOnce({ success: true })
  const testData = { food: test food, drink: test drink }
  const { getByLabelText, getByText, findByText } = render(<App />)

  // use regex
  fireEvent.click(getByText(/fill.*form/i))

  // pass the data
  fireEvent.change(getByLabelText(/food/i), {
    target: { value: testData.food },
  })
  fireEvent.click(getByText(/next/i))

  fireEvent.change(getByLabelText(/drink/i), {
    target: { value: testData.drink },
  })
  fireEvent.click(getByText(/review/i))

  expect(getByLabelText(/food/i)).toHaveTextContent(testData.food)
  expect(getByLabelText(/drink/i)).toHaveTextContent(testData.drink)

  // solve multi confirm text, add selector
  fireEvent.click(getByText(/confirm/i, { selector: ‘button‘ }))

  expect(mockSubmitForm).toHaveBeenCalledWith(testData)
  expect(mockSubmitForm).toHaveBeenCalledTimes(1)

  // findBy*, using await
  fireEvent.click(await findByText(/home/i))

  expect(getByText(/welcome home/i)).toBeInTheDocument()
})

 

Imporved version: by findBy*

By using some of the get queries, we’re assuming that those elements will be available on the page right when we execute the query. This is a bit of an implementation detail and it’d be cool if we could not make that assumption in our test. Let’s swap all those for find queries.

jest.mock(../extra/api)

afterEach(() => {
  jest.clearAllMocks()
})

test(Can fill out a form across multiple pages, async () => {
  mockSubmitForm.mockResolvedValueOnce({ success: true })
  const testData = { food: test food, drink: test drink }
  const { findByLabelText, findByText } = render(<App />)

  fireEvent.click(await findByText(/fill.*form/i))

  fireEvent.change(await findByLabelText(/food/i), {
    target: { value: testData.food },
  })
  fireEvent.click(await findByText(/next/i))

  fireEvent.change(await findByLabelText(/drink/i), {
    target: { value: testData.drink },
  })
  fireEvent.click(await findByText(/review/i))

  expect(await findByLabelText(/food/i)).toHaveTextContent(testData.food)
  expect(await findByLabelText(/drink/i)).toHaveTextContent(testData.drink)

  fireEvent.click(await findByText(/confirm/i, { selector: button }))

  expect(mockSubmitForm).toHaveBeenCalledWith(testData)
  expect(mockSubmitForm).toHaveBeenCalledTimes(1)

  fireEvent.click(await findByText(/home/i))

  expect(await findByText(/welcome home/i)).toBeInTheDocument()
})

 

Improved version: user-event

import user from @testing-library/user-event

test(Can fill out a form across multiple pages, async () => {
  mockSubmitForm.mockResolvedValueOnce({ success: true })
  const testData = { food: test food, drink: test drink }
  const { findByLabelText, findByText } = render(<App />)

  user.click(await findByText(/fill.*form/i))

  user.type(await findByLabelText(/food/i), testData.food)
  user.click(await findByText(/next/i))

  user.type(await findByLabelText(/drink/i), testData.drink)
  user.click(await findByText(/review/i))

  expect(await findByLabelText(/food/i)).toHaveTextContent(testData.food)
  expect(await findByLabelText(/drink/i)).toHaveTextContent(testData.drink)

  user.click(await findByText(/confirm/i, { selector: button }))

  expect(mockSubmitForm).toHaveBeenCalledWith(testData)
  expect(mockSubmitForm).toHaveBeenCalledTimes(1)

  user.click(await findByText(/home/i))

  expect(await findByText(/welcome home/i)).toBeInTheDocument()
})

 

[React Testing] Write React Application Integration Tests with React Testing Library

上一篇:Active Learning: 一个降低深度学习时间,空间,经济成本的解决方案|CVPR 2017


下一篇:监测系统电信性能