Если вы спросите любого разработчика о тестах, он ответит, что тесты необходимы. Они указывают на то, что код работает так, как задумано, и что ваше новое изменение ничего не сломало. Однако, если вы зайдете практически в любой проект React, то сможете заметить, что их тесты не очень хороши. Многие из них имеют огромное количество моментальных тестов и, возможно, несколько сквозных тестов. Нет ни надлежащих модульных тестов, ни тестирования событий. Так почему же? Мое мнение о том, как создаются компоненты. Они слишком велики и имеют слишком много логики внутри. И в этом посте я объясняю, как, по моему мнению, вы должны структурировать компоненты для их тестирования.

Почему ваши компоненты нельзя тестировать?

Прежде чем объяснять, как структурировать ваш компонент, давайте рассмотрим две важные вещи, из-за которых их нелегко тестировать. И это область видимости JavaScript, а не использование чистых функций.

Область определения JavaScript

Обсуждая область определения, я имею в виду области кода, в которых видна ваша переменная или функция. В JavaScript у нас есть область видимости функции. Это означает, что все, что определено в функции, видимо в этой функции, но не за ее пределами. Сегодня мы в основном используем в React компоненты без состояния, и это функции. Сочетание этого с тем, как работает область действия JavaScript, означает, что все, что определено внутри компонента, недоступно снаружи. Это также означает, что вы можете протестировать действие функции, определенной внутри компонента, но не самой функции, поскольку она не видна вашим тестам. И сразу же, это не правильный модульный тест.

Чистые функции

Прежде чем понять, почему неиспользование чистой функции является проблемой, вам нужно понять, что такое чистая функция. Глядя на определение, в нем говорится, что есть два требования, чтобы функция была чистой. Во-первых, одни и те же аргументы дают один и тот же результат, а во-вторых, нет побочных эффектов. Так что это значит?

const name = “John”
 
function greeting() {
  return `Hello, ${name}`;
}

Если мы посмотрим на пример выше, эта функция не является чистой, поскольку нарушает первое правило. Имя, используемое для приветствия, определяется вне функции и не передается в качестве параметра. Это означает, что функция может возвращать разные результаты для разных запусков в зависимости от значения некоторой внешней переменной. Если вы хотите протестировать эту функцию, вам нужно сначала определить значение этой внешней переменной. И надеюсь, что что-то не перекроет его. Это то, что часто происходит с компонентами React, так как многие используют подобные реквизиты. Но мы могли бы исправить это, передав имя в качестве аргумента функции, и тогда это стало бы чистой функцией.

const name = “John”
function greeting(personName) {
    return `Hello, ${personName}`;
}
greeting(name);

Второе требование сегодня встречается немного реже. Это происходит, когда ваша функция пытается изменить значение переменных за пределами своей области. Использование предыдущего примера означало бы изменение переменной значения приветствия внутри функции.

const name = “John”
let greetingText;
function greeting(personName) {
    greetingText = `Hello, ${personName}`;
}
greeting(name);

Вы можете исправить это, заставив функцию возвращать значение приветствия, а не изменять его внутри.

const name = “John”
function greeting(personName) {
    return `Hello, ${personName}`;
}
let greetingText = greeting(name)

Создание компонента для тестирования

Исключить из службы

Теперь мы можем рассказать, как сделать компоненты тестируемыми. И для этого я начинаю с простого, уже сделанного компонента. Все, что есть у этого компонента, — это поле ввода и div, который показывает все числа, удаленные из этого текста.

Если вы посмотрите на код ниже, это не сложный компонент. Две функции. Один для обработки даже изменений и один для удаления чисел из строки. Но как бы вы проверили эту функцию?

function DemoApp() {
  const [value, setValue] = useState(“”);
  const [cleanValue, setCleanValue] = useState(“”);
 
  function stripNumbers(text) {
    return text.replace(/\d+/g, “”);
  }
 
  function handleChange(ev) {
    const newValue = ev.target.value;
    setValue(newValue);
    setCleanValue(stripNumbers(newValue));
  }
 
  return (
    <>
      <div>
        <input value={value} onChange={handleChange}/>
      </div>
      <div>{cleanValue}</div>
    </>
  )
}

Вы можете визуализировать компонент, инициировать события изменения при вводе, а затем протестировать содержимое этого div. Это не модульный тест. И вы не можете протестировать его самостоятельно, так как это частная функция. Лучшим вариантом было бы исключить функцию в отдельный служебный файл.

import stripNumbers from “./stripNumbers”;
 
function DemoApp() {
  const [value, setValue] = useState(“”);
  const [cleanValue, setCleanValue] = useState(“”);
 
  function handleChange(ev) {
    const newValue = ev.target.value;
    setValue(newValue);
    setCleanValue(stripNumbers(newValue));
  }
 
  return (
    <>
      <div>
        <input value={value} onChange={handleChange}/>
      </div>
      <div>{cleanValue}</div>
    </>
  )
}
// stripNumbers.js
function stripNumbers(text) {
  return text.replace(/\d+/g, “”);
}
 
export default stripNumbers;

Теперь вы можете импортировать эту функцию и беспрепятственно запускать с ней тесты.

Разбивайте компоненты на мелкие части

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

Для этого мы могли бы разместить все в одном компоненте. И это не неправильное решение, и если вы посмотрите на код, он легко читается и понимается.

function PeopleList({people}) {
  function getPeopleList(people) {
   return people.map(({firstName, lastName, dob}, index) => (
     <div key={`person-${index}`}>
       <div>First name: {firstName}</div>
       <div>Last name: {lastName}</div>
       <div>Date of Birth: {dob}</div>
     </div>
    ))
  }
 
  return (
    <div>
      {getPeopleList(people)}
    </div>
  )
}

Так почему и что мы хотели бы улучшить? Что мы можем сделать, чтобы упростить тестирование этого компонента? Как и в предыдущем примере, мы можем исключить функцию из отдельной службы и провести ее модульное тестирование. Но я хочу сосредоточиться на размере компонента. В функциях не должно быть много логики. И то же самое с компонентами. Поэтому предлагаемое решение состоит в том, чтобы исключить данные о человеке в отдельный компонент.

function Person({firstName, lastName, dob}) {
  return (
    <>
      <div>First name: {firstName}</div>
      <div>Last name: {lastName}</div>
      <div>Date of Birth: {dob}</div>
    </>
  ) 
}
 
function PeopleList({people}) {
  function getPeopleList(people) {
    return people.map((person, index) => (
      <div key={`person-${index}`}>
        <Person {…person} />
      </div>
    ))
  }
 
  return (
    <div>
      {getPeopleList(people)}
    </div>
  )
}

Теперь у вас есть два компонента для обработки. Но если вы хотите проверить только то, как отображаются данные одного человека. Вы можете сделать это. Вам не нужно отображать весь список, только для тестирования одного экземпляра. Меньшие компоненты более удобны для повторного использования и тестирования.

Подведение итогов

В этом тоже есть элемент здравого смысла. Я хотел проиллюстрировать два действия, которые вы можете сделать, чтобы упростить тестирование. Но не всегда они будут иметь смысл. Чтобы решить, когда что-то исключить или разделить, вы можете спросить себя, хотите ли вы протестировать это самостоятельно. Или у вас возникли трудности с его тестированием из-за вещей, не связанных с вашим компонентом или функцией? Если вы тратите время на то, чтобы издеваться над разными реквизитами или другими сервисами, которыми вы не пользуетесь в области, над которой издеваетесь, разделите их. Наличие большого количества файлов может показаться пугающим, но у вас не должно возникнуть этой проблемы при правильной структуре и правильном именовании. И сегодня большинство IDE имеют отличные возможности поиска.

Есть ли у вас какие-либо предложения или рекомендации, которые вы используете для тестирования? Напишите их в комментарии.

Чтобы узнать больше, вы можете подписаться на меня в Twitter, LinkedIn, GitHub или Instagram.