Уроки, которые я получил, будучи новичком в Jest и React Testing Library

Недавно я начал использовать Jest и React Testing Library (rtl). Эта статья — то, что я хотел бы прочитать, когда только начинал. Это не учебник, а серия решений конкретных проблем, с которыми вы столкнетесь. Я разбил все на 4 блока:

  1. matchers
  2. setup functions

Эта статья охватывает первые 3 блока. О mocking я напишу в более поздней серии. Весь код в этой статье вы можете найти на github.


1. Запросы

React Testing Library предоставляет запросы и рекомендации по их использованию. Вот несколько советов и рекомендаций относительно запросов.

1.1 Вы все еще можете использовать querySelector.

Прежде чем вы начнете добавлять data-testid ко всему, помните, что Jest — это просто javascript. Функция expect() ожидает передачи элемента DOM. Поэтому вы можете использовать querySelector. Вызовите querySelector на container, возвращенном функцией render.

// the component
function Component1(){
  return(
    <div className="Component1">
      <h4>Component 1</h4>
      <p>Lorum ipsum.</p>
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима
// the test
test('Component1 renders', () => {
  // destructure container out of render result
  const { container } = render(<MyComponent />)
  // true
  // eslint-disable-next-line
  expect(container.querySelector('.Component1')).toBeInTheDocument()
})
Войти в полноэкранный режим Выход из полноэкранного режима

querySelector хорош для тестирования вашего общего html. Но по возможности всегда используйте конкретные rtl-запросы: например, для ввода, кнопок, изображений, заголовков…

Осторожно, использование querySelector заставит eslint накричать на вас, отсюда // eslint-disable-next-line.

1.2 Как запросить несколько одинаковых элементов

Возьмем компонент с двумя кнопками, сложение и вычитание. Как запросить эти кнопки? У вас есть 2 варианта:

// the component
function Component2(){
  return(
    <div className="Component2">
      <h4>Component 2</h4>
      <button>add</button>
      <button>subtract</button>
    </div>
  )
}
Войти в полноэкранный режим Выйти из полноэкранного режима

1.2.1 Использовать параметр options в запросе

Большинство запросов rtl имеют необязательный параметр options. Это позволяет вам выбрать конкретный элемент, который вам нужен. В данном случае мы используем name.

test('Component2 renders', () => {
  render(<Component2 />)
  // method 1
  expect(screen.getByRole('button', { name: 'subtract' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'add' })).toBeInTheDocument()
})
Вход в полноэкранный режим Выйти из полноэкранного режима

1.2.2 Использование запроса getAll

React Testing Library имеет встроенные запросы для нескольких элементов, getAllBy.... Эти запросы возвращают массив.

test('Component2 renders', () => {
  render(<Component2 />)
  // method 2
  const buttons = screen.getAllByRole('button')
  expect(buttons[0]).toBeInTheDocument()
  expect(buttons[1]).toBeInTheDocument()
})
Вход в полноэкранный режим Выход из полноэкранного режима

1.3 Поиск нужной роли

Некоторые элементы html имеют определенные роли ARIA. Вы можете найти их на этой странице w3.org. (Совет по закладкам) Некоторые примеры:

// the component
function Component3(){
  const [ count, setCount ] = useState(0)
  return(
    <div className="Component3">
      <h4>Component 3</h4>
      <input type="number" value={count} onChange={(e) => setCount(parseInt(e.target.value))} />
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима
// the test
test('Component3 renders', () => {
  render(<Component3 />)
  // get the heading h3
  expect(screen.getByRole('heading', { level: 3 })).toBeInTheDocument()
  // get the number input
  expect(screen.getByRole('spinbutton')).toBeInTheDocument()
})
Войти в полноэкранный режим Выйти из полноэкранного режима

2. матчеры

Ядром теста является мачер, оператор expect(), за которым следует .toBe... или .toHave.... Это Jest, это не React Testing Library. Потратьте немного времени на знакомство с этими матчерами (еще один совет по закладкам).

Помимо этих Jest матчеров, существует дополнительная библиотека: jest-dom (да, еще одна закладка).

jest-dom — это библиотека-компаньон для Testing Library, которая предоставляет пользовательские матрицы элементов DOM для Jest.

Итак, jest-dom предоставляет больше мачеров, и они довольно удобны. Давайте посмотрим на некоторые из них в действии. Я написал несколько тестов на jest, а затем эквивалент jest-dom.

// the component
function Component4(){
  const [ value, setValue ] = useState("Wall-E")
  return(
    <div className="Component4">
      <h4>Component 4</h4>
      <label htmlFor="movie">Favorite Movie</label>
      <input 
        id="movie"
        value={value} 
        onChange={(e) => setValue(e.target.value)} 
        className="Component4__movie"
        style={{ border: '1px solid blue', borderRadius: '3px' }} 
        data-value="abc" />
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима
test('Component4 renders', () => {

  render(<Component4 />)
  const input = screen.getByLabelText('Favorite Movie')
  const title = screen.getByRole('heading', { level: 4 })

  // we already used .toBeInTheDocument(), this is jest-dom matcher
  expect(input).toBeInTheDocument()

  // test for class with jest
  expect(input.classList.contains('Component4__movie')).toBe(true)
  // test for class with jest-dom
  expect(input).toHaveClass('Component4__movie')

  // test for style with jest
  expect(input.style.border).toBe('1px solid blue')
  expect(input.style.borderRadius).toBe('3px')
  // test for style with jest-dom
  expect(input).toHaveStyle({
    border: '1px solid blue', 
    borderRadius: '3px',
  })

  // test h4 value with jest
  expect(title.textContent).toBe("Component 4")
  // test h4 value with jest-dom
  expect(title).toHaveTextContent("Component 4")

  // test input data attribute with jest
  expect(input.dataset.value).toEqual('abc')
  // test input data attribute with jest-dom
  expect(input).toHaveAttribute('data-value', 'abc')
})
Вход в полноэкранный режим Выход из полноэкранного режима

3. настройки рендеринга

Написание тестов для компонентов может быть повторяющимся и отнимать много времени. Давайте рассмотрим, как функция настройки может сделать ваш код более DRY (не повторяйтесь).

Мы будем тестировать компонент, который отображает значение. Он имеет кнопки сложения и вычитания и принимает инкремент (число) в качестве реквизита. Кнопки прибавляют или отнимают приращение от значения.

// the component
function Component5({ increment }){
  const [ value, setValue ] = useState(0)
  return(
    <div className="Component5">
      <h4>Component 5</h4>
      <div className="Component5__value">{value}</div>
      <div className="Component5__controles">
        <button onClick={e => setValue(prevValue => prevValue - increment)}>subtract</button>
        <button onClick={e => setValue(prevValue => prevValue + increment)}>add</button>
      </div>
    </div>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы выполним 3 теста на этом компоненте: тест на отображение компонента, тест на работу кнопок, тест на инкремент. Сначала мы запустим не DRY код. После этого мы рефакторим тесты с помощью функции setup.

// the tests
describe('Component5 (not DRY)', () => {
  test('It renders correctly', () => {
    const { container } = render(<Component5 increment={1} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })

    // do the tests
    // eslint-disable-next-line
    expect(container.querySelector('.Component5')).toBeInTheDocument()
    expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Component 5')
    expect(valueEl).toBeInTheDocument()
    expect(valueEl).toHaveTextContent('0')
    expect(subtractButton).toBeInTheDocument()
    expect(addButton).toBeInTheDocument()
  })

  test('It changes the value when the buttons are clicked', () => {
    const { container } = render(<Component5 increment={1} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })

    // test default value
    expect(valueEl).toHaveTextContent('0')
    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('1')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })

  test('It adds or subtract the increment 10', () => {
    const { container } = render(<Component5 increment={10} />)

    // get the elements
    // eslint-disable-next-line
    const valueEl = container.querySelector('.Component5__value')
    const subtractButton = screen.getByRole('button', { name: 'subtract' })
    const addButton = screen.getByRole('button', { name: 'add' })

    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('10')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })
})
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, здесь много дублирования. Мы делаем один и тот же рендер и одни и те же запросы во всех тестах. Сейчас мы перепишем эти тесты. Начнем с добавления этой функции в корень файла:

function setup(props){
  const { container } = render(<Component5 {...props} />)
  return{
    // eslint-disable-next-line
    valueEl: container.querySelector('.Component5__value'),
    subtractButton: screen.getByRole('button', { name: 'subtract' }),
    addButton: screen.getByRole('button', { name: 'add' }),
    container,
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Позвольте мне провести вас через эту функцию:

  1. Мы переместили render() внутрь нашей функции setup. Когда вызывается setup, происходит рендеринг компонента.

  2. Функция render() по-прежнему возвращает container, поэтому у нас есть доступ к этому элементу внутри нашей функции setup.

  3. Теперь мы распространяем аргумент setup (props, объект) в наш компонент: {...props}. Этот паттерн позволяет использовать одну и ту же функцию setup с разными props.

    setup({ increment: 1 })
    // calls render(<Component5 increment="1">)
    setup({ increment: 5 })
    // calls render(<Component5 increment="5">)
    
  4. Из нашей функции setup мы возвращаем объект со всеми нашими часто используемыми запросами (кнопки и элемент value). Это дает нам доступ к этим запросам внутри теста, где вызывается функция setup.

    test('It renders', () => {
      const { valueEl, subtractButton, addButton } = setup({ increment: 1 })
      // do tests with these elements
    })
    
  5. Наконец, я также поместил container на возвращаемый объект. Это дает нам доступ к контейнеру внутри test() для запросов, которые, например, вы используете только один раз.

    test('It renders', () => {
      const { container } = setup({ increment: 1 })
      // eslint-disable-next-line
      expect(container.querySelector('.Component5')).toBeInTheDocument()
    })
    

В заключение: обновленный тест с этой функцией настройки:

// the setup function
function setup(props){
  const { container } = render(<Component5 {...props} />)
  return{
    // eslint-disable-next-line
    valueEl: container.querySelector('.Component5__value'),
    subtractButton: screen.getByRole('button', { name: 'subtract' }),
    addButton: screen.getByRole('button', { name: 'add' }),
    container,
  }
}
// the tests
describe('Component 5 (DRY)', () => {
  test('It renders', () => {
    const { container, valueEl, subtractButton, addButton } = setup({ increment: 1 })
    // do the tests
    // eslint-disable-next-line
    expect(container.querySelector('.Component5')).toBeInTheDocument()
    expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Component 5')
    expect(valueEl).toBeInTheDocument()
    expect(valueEl).toHaveTextContent('0')
    expect(subtractButton).toBeInTheDocument()
    expect(addButton).toBeInTheDocument()
  })

  test('It changes the value when the buttons are clicked', () => {
    const { valueEl, subtractButton, addButton } = setup({ increment: 1 })

    // test default value
    expect(valueEl).toHaveTextContent('0')
    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('1')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })

  test('It adds or subtract the increment 10', () => {
    const { valueEl, subtractButton, addButton } = setup({ increment: 10 })

    // test addbutton
    userEvent.click(addButton)
    expect(valueEl).toHaveTextContent('10')
    // test subtract button
    userEvent.click(subtractButton)
    expect(valueEl).toHaveTextContent('0')
  })
})
Вход в полноэкранный режим Выход из полноэкранного режима

Это все еще может показаться большим количеством кода, но это намного чище. Этот шаблон сэкономит вам много времени и позволит избежать повторений.


Заключение

Мы рассмотрели тестирование queries, matchers и setup functions. Я предложил решения проблем, с которыми вы можете столкнуться. Надеюсь, это даст вам больше практических знаний о тестировании компонентов react.

Я написал серию статей о мокинге React, которая является хорошим продолжением этой статьи.

Оцените статью
devanswers.ru
Добавить комментарий