Тестирование useState через компонент

useState — это один из самых часто используемых хуков в React для управления состоянием компонента. При написании юнит-тестов для компонентов, использующих useState, важно правильно имитировать изменения состояния и убедиться в корректности работы логики компонента, который использует это состояние. В данной статье рассмотрим подходы к тестированию компонентов с использованием useState.

1. Установка необходимых зависимостей

Для начала важно убедиться, что в проекте установлены нужные библиотеки для тестирования компонентов React. В большинстве случаев используется Jest в связке с Enzyme для удобного тестирования React-компонентов.

npm install --save-dev jest enzyme enzyme-adapter-react-16

После установки Enzyme необходимо настроить адаптер для React:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

2. Пример компонента с useState

Рассмотрим простой компонент, использующий хук useState для хранения состояния счетчика:

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onCl ick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

Этот компонент содержит счетчик, который увеличивается при нажатии на кнопку.

3. Тестирование состояния через Enzyme

Для тестирования компонента с useState мы можем воспользоваться shallow rendering из Enzyme, который позволяет рендерить компонент на одном уровне, не рендеря дочерние компоненты. С помощью этого метода можно проверить изменения состояния в компоненте без необходимости рендерить все его дочерние элементы.

3.1 Проверка начального состояния

Первоначально проверим, что компонент правильно отображает начальное состояние:

import React from 'react';
import { shallow } from 'enzyme';
import Counter from './Counter';

describe('Counter component', () => {
  it('should render initial state correctly', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.find('p').text()).toBe('Count: 0');
  });
});

В данном тесте мы ожидаем, что при первоначальном рендеринге компонента текст в элементе <p> будет равен “Count: 0”, так как начальное значение состояния count равно нулю.

3.2 Тестирование изменения состояния

Следующий шаг — это тестирование изменения состояния. Для этого проверим, изменяется ли текст в компоненте при нажатии на кнопку, которая вызывает функцию increment:

describe('Counter component', () => {
  it('should update the count when the button is clicked', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.find('p').text()).toBe('Count: 0');
    
    // Симуляция клика по кнопке
    wrapper.find('button').simulate('click');
    
    // Проверка, что состояние обновилось
    expect(wrapper.find('p').text()).toBe('Count: 1');
  });
});

В этом тесте мы сначала проверяем, что начальное состояние компонента равно 0. Затем симулируем клик по кнопке и проверяем, что значение состояния изменилось на 1.

3.3 Тестирование нескольких кликов

Для проверки корректности работы компонента при нескольких кликах можно написать тест, который симулирует несколько нажатий на кнопку и проверяет последовательное изменение состояния.

describe('Counter component', () => {
  it('should update the count correctly after multiple clicks', () => {
    const wrapper = shallow(<Counter />);
    
    expect(wrapper.find('p').text()).toBe('Count: 0');
    
    // Симулируем несколько кликов
    wrapper.find('button').simulate('click');
    expect(wrapper.find('p').text()).toBe('Count: 1');
    
    wrapper.find('button').simulate('click');
    expect(wrapper.find('p').text()).toBe('Count: 2');
    
    wrapper.find('button').simulate('click');
    expect(wrapper.find('p').text()).toBe('Count: 3');
  });
});

Этот тест проверяет, что при каждом клике значение счетчика увеличивается на 1.

4. Тестирование с использованием spy-функций

Когда необходимо протестировать логику изменения состояния или вызов побочных эффектов, можно использовать jest.fn() или spyOn() для отслеживания вызова функций.

Предположим, у нас есть компонент, который вызывает дополнительную функцию при изменении состояния:

import React, { useState } from 'react';

const CounterWithCallback = ({ onIncrement }) => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
    onIncrement(count + 1); // вызываем callback
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onCl ick={increment}>Increment</button>
    </div>
  );
};

export default CounterWithCallback;

Теперь, при изменении состояния, вызывается дополнительная функция onIncrement. Тестируем этот компонент следующим образом:

describe('CounterWithCallback component', () => {
  it('should call the onIncrement callback when the state changes', () => {
    const onIncrem ent = jest.fn(); // создаём mock-функцию
    const wrapper = shallow(<CounterWithCallback onIncrem ent={onIncrement} />);

    wrapper.find('button').simulate('click');
    
    // Проверка, что onIncrement была вызвана с правильным аргументом
    expect(onIncrement).toHaveBeenCalledWith(1);
  });
});

В этом тесте мы создаём mock-функцию onIncrement и проверяем, что она была вызвана с правильным значением после изменения состояния.

5. Использование mount для более глубокой проверки

Когда необходимо протестировать полный цикл компонента, включая дочерние элементы, используется mount вместо shallow рендеринга. Это позволяет выполнить полный рендер компонента и его дочерних элементов, что полезно, например, при тестировании взаимодействия с контекстами или эффектами.

import { mount } from 'enzyme';

describe('Counter component with mount', () => {
  it('should update the count with mount', () => {
    const wrapper = mount(<Counter />);
    
    expect(wrapper.find('p').text()).toBe('Count: 0');
    
    wrapper.find('button').simulate('click');
    expect(wrapper.find('p').text()).toBe('Count: 1');
  });
});

mount требует более высоких ресурсов для рендеринга, но позволяет проверить взаимодействие компонентов на более глубоком уровне, включая все побочные эффекты.

6. Важные замечания

  • setState работает асинхронно, поэтому важно использовать методы Enzyme, такие как update(), для получения актуального состояния после изменения.
  • Для сложных компонентов с побочными эффектами стоит использовать useEffect и тестировать его с помощью Enzyme, чтобы убедиться, что побочные эффекты корректно выполняются.

Тестирование компонентов с useState позволяет убедиться, что состояние обновляется корректно и компонент ведет себя как ожидается. Важно помнить, что для каждого теста следует выбирать оптимальный метод рендеринга (shallow, mount), а также учитывать асинхронность изменений состояния.