Написание React Test с библиотеками React рекомендуют - Jest и React Testing Library для полного новичка.

Эта статья предназначена для тех, кто только начинает изучать React и задается вопросом, как написать несколько простых тестов с помощью своих приложений React. И j. Подобно тому, как большинство людей начинают создавать приложение React, используя create-react-app, я бы тоже начал с него.

Во-первых, давайте начнем с примера по умолчанию.

Зависимости по умолчанию с create-react-app (22.05.2020)

"dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  }

Уже написан один тест, который поможет вам начать.

// src/App.test.js
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
  const { getByText } = render(<App />); //render is from @testing-library/react
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument(); //expect assertion is from Jest
});

Если вы запустите команду $ yarn test App, вы увидите результат, аналогичный следующему:

С настройкой по умолчанию create-react-app вы можете начать писать тест, ничего не устанавливая и не настраивая.

Из приведенного выше примера мы должны узнать -

  • Где и как я могу разместить свои тестовые файлы? - как вы можете видеть, App.test.js помещается рядом с App.js в той же папке, а суффикс .test.js после имени компонента App в качестве имени файла. Это соглашения по умолчанию, предложенные командой create-react-app (ссылка здесь).
  • Jest и React Testing Library - это цепочка инструментов, стоящая за тестом, они оба поставляются с приложением create-react-app по умолчанию.
// setupTests.js
// Jest is importing from a global setup file if you wonder
import '@testing-library/jest-dom/extend-expect';

Во-вторых, напишите тест для компонента NavBar.

Я создаю NavBar компонент, содержащий ссылки и логотип.

Во-первых, я бы начал писать тест без написания самого компонента (Test Drive Development).

// navBar.test.js
import React from 'react'; 
// screen newer way to utilize query in 2020 
import { render, screen } from '@testing-library/react'; 
import NavBar from './navBar'; // component to test
test('render about link', () => {
  render(<NavBar />);
  expect(screen.getByText(/about/)).toBeInTheDocument();
})

Сначала тест завершится неудачно, поскольку я еще не писал кода для navBar.js компонента.

Тогда давайте закончим компонентnavBar.js, теперь тест должен пройти.

// navBar.js
import React from 'react';
const NavBar = () => (
  <div className="navbar">
    <a href="#">
      about
    </a>
  </div>
);
export default NavBar;

А пока вам следует узнать:

  • expect( ... ).toBeInTheDocument() утверждение от Jest.
  • render(<NavBar />); и screen.getByText(/about/) из библиотеки тестирования.
  • Библиотека тестирования Jest и React работает вместе, чтобы упростить написание тестов в React.
  • screen.getByText(/about/) использование getByText вместо выбора по имени класса связано с тем, что библиотека тестирования React адаптирует образ мышления, ориентированный на взаимодействие с пользователем, а не на детали реализации.

Чтобы узнать больше о расширении и изменении теста, вы можете ознакомиться со следующими ресурсами:

Теперь давайте расширим тест и компонент, чтобы сделать его более реальным -

// navBar.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import NavBar from './navBar';
// include as many test cases as you want here
const links = [
  { text: 'Home', location: "/" },
  { text: 'Contact', location: "/contact" },
  { text: 'About', location: "/about" },
  { text: 'Search', location: "/search" },
];
// I use test.each to iterate the test cases above
test.each(links)(
  "Check if Nav Bar have %s link.",
  (link) => {
    render(<NavBar />);
    //Ensure the text is in the dom, will throw error it can't find
    const linkDom = screen.getByText(link.text); 
		
    //use jest assertion to verify the link property
    expect(linkDom).toHaveAttribute("href", link.location);
  }
);
test('Check if have logo and link to home page', () => {
    render(<NavBar />);
    // get by TestId define in the navBar
    const logoDom = screen.getByTestId(/company-logo/); 
    // check the link location
    expect(logoDom).toHaveAttribute("href", "/"); 
    //check the logo image
  expect(screen.getByAltText(/Company Logo/)).toBeInTheDocument(); 
});

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

// navBar.js
import React from 'react';
const NavBar = () => (
  <div className="navbar">
    <a href="/" data-testid="company-logo">
      <img src="/logo.png" alt="Company Logo" />
    </a>
    <ul>
      <li>
        <a href="/"> Home </a>
      </li>
      <li>
        <a href="/about"> About </a>
      </li>
      <li>
        <a href="/contact"> Contact </a>
      </li>
      <li>
        <a href="/search"> Search </a>
      </li>
    </ul>
  </div>
);
export default NavBar;

Хорошо, это кажется забавным, давайте напишем более сложный.

В-третьих, напишите тест компонента формы регистрации.

После написания теста на статический контент давайте напишем тест на более динамичный контент - форму регистрации.

Во-первых, давайте подумаем в духе TDD - что нам нужно в этой форме регистрации (независимо от того, как она выглядит):

  • Поле ввода для имени, которое допускает длину только строки от 3 до 30.
  • Поле ввода для электронной почты, которое может проверить, является ли это действительным адресом электронной почты.
  • Поле ввода пароля, в котором можно проверить его сложность (минимум 1 число, 1 строка в нижнем регистре, 1 строка в верхнем регистре, 1 специальный символ)
  • Кнопка отправки.
  • Все 3 поля, указанные выше, являются обязательными, не могут быть пустыми.

А теперь давайте напишем тест.

/*  Prepare some test cases, ensure 90% edge cases are covered.
    You can always change your test cases to fit your standard
*/
const entries = [
  { name: 'John', email: 'john_doe@yahoo', password: 'helloworld' },
  { name: 'Jo', email: 'jo.msn.com', password: 'pa$$W0rd' },
  { name: '', email: '[email protected]', password: '123WX&abcd' },
  { name: 'kent'.repeat(10), email: '[email protected]', password: 'w%oRD123yes' },
  { name: 'Robert', email: '[email protected]', password: 'r&bsEc234E' },
]

Затем создайте череп теста.

// signupForm.test.js
// this mostly a input validate test
describe('Input validate', () => {
/* 
   I use test.each to iterate every case again
   I need use 'async' here because wait for 
   validation is await function 
*/ 
 test.each(entries)('test with %s entry', async (entry) => { 
    ...
 
  })
})

Теперь давайте построим блок внутри теста.

// signupForm.test.js
...
test.each(entries)('test with %s entry', async (entry) => { 
//render the component first (it will clean up for every iteration    
render(<SignupForm />); 
		
/*  grab all the input elements. 
    I use 2 queries here because sometimes you can choose
    how your UI look (with or without Label text) without
    breaking the tests
*/	 
    const nameInput = screen.queryByLabelText(/name/i)
      || screen.queryByPlaceholderText(/name/i);
    const emailInput = screen.getByLabelText(/email/i)
      || screen.queryByPlaceholderText(/email/i);
    const passwordInput = screen.getByLabelText(/password/i)
      || screen.queryByPlaceholderText(/password/i);
		
/* use fireEvent.change and fireEvent.blur 
   to change name input value
   and trigger the validation
*/
    fireEvent.change(nameInput, { target: { value: entry.name } }); 
    fireEvent.blur(nameInput); 
/* first if-statement to check whether the name is input.
   second if-statement to check whether the name is valid.
   'checkName' is a utility function you can define by yourself.
   I use console.log here to show what is being checked.  
*/
  if (entry.name.length === 0) {
      expect(await screen.findByText(/name is required/i)).not.toBeNull();
      console.log('name is required.');
    }
    else if (!checkName(entry.name)) {
// if the name is invalid, error msg will showup somewhere
    expect(await screen.findByText(/invalid name/i)).not.toBeNull();
      console.log(entry.name + ' is invalid name.');
    };
		
// With a similar structure, you can continue building the rest of the test.
		...
/*  Remember to add this line at the end of your test to 
    avoid act wrapping warning.
    More detail please checkout Kent C.Dodds's post:
    (He is the creator of Testing Library)    <https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning>
*/
  await act(() => Promise.resolve()); 
})
...

Полный код тестирования можно найти здесь.

Хорошо, теперь тест завершен (возможно, мы вернемся, чтобы немного подправить, но давайте пока продолжим), давайте напишем компонент.

// signupForm.js
import React from 'react';
/* 
I borrow the sample code from formik library with some adjustments
<https://jaredpalmer.com/formik/docs/overview#the-gist>
*/
import { Formik } from 'formik';
/* 
For validation check, I wrote 3 custom functions.
(I use the same functions in test)
*/
import {
  checkName,
  checkEmail,
  checkPassword,
} from '../utilities/check';
const SignupForm = () => (
  <div>
    <h1>Anywhere in your app!</h1>
    <Formik
      initialValues={{ name: '', email: '', password: '' }}
      validate={values => {
        const errors = {};
        if (!values.name) {
          errors.name = 'Name is Required'
        } else if (!checkName(values.name)) {
          errors.name = `invalid name`;
        }
        if (!values.email) {
          errors.email = 'Email is Required';
        }
        else if (!checkEmail(values.email)) {
          errors.email = 'Invalid email address';
        }
        if (!values.password) {
          errors.password = 'Password is Required';
        } else if (!checkPassword(values.password)) {
          errors.password = 'Password is too simple';
        }
        return errors;
      }}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      {({
        values,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting,
        /* and other goodies */
      }) => (
          <form onSubmit={handleSubmit}>
            <label>
              Name:
            <input
                type="text"
                name="name"
                placeholder="Enter your name here"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.name}
              />
            </label>
            <p style={{ 'color': 'red' }}>
              {errors.name && touched.name && errors.name}
            </p>
            <label>
              Email:
            <input
                type="email"
                name="email"
                placeholder="Your Email Address"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.email}
              />
            </label>
            <p style={{ 'color': 'red' }}>
              {errors.email && touched.email && errors.email}
            </p>
            <label>
              Password:
            <input
                type="password"
                name="password"
                placeholder="password here"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.password}
              />
            </label>
            <p style={{ 'color': 'red' }}>
            {errors.password && touched.password && errors.password}
            </p>
            <button type="submit" disabled={isSubmitting}>
              Submit
          </button>
          </form>
        )}
    </Formik>
  </div>
);
export default SignupForm;

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

И при неправильном вводе сообщение об ошибке будет отображаться под вводом:

Если вы завершили тест, описанный выше, теперь все тесты должны пройти, запустить yarn test --verbose, с параметром подробного вывода и сообщением console.log, вы можете увидеть, как тестируется каждый случай, и какой из них является хорошим, а какой нет.

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

Заключительные слова.

Новичку сложно выучить все сразу, поэтому просто не спешите, если это слишком сложно. На изучение основ у меня ушла как минимум целая неделя, и это только начало написания тестов для приложений React.

Это трудная тема для понимания, но я считаю, что стоит потратить на нее некоторое время, если вы хотите стать Pro FrontEnd разработчиком.

И хорошая новость в том, что у вас хорошее начало, теперь вы должны знать, как использовать Jest и React Testing Library для написания тестов для ваших реагирующих компонентов, и вы можете начать чтобы изучить другие библиотеки и решения на этой хорошей основе.

Я планирую написать еще одну статью, чтобы охватить больше передовых примеров, если я получу положительный отзыв об этой статье. Еще раз спасибо за ваше время.

Ресурсы, на которые я ссылался при написании этой статьи

Особые благодарности ooloo.io и Johannes Kettmann

Тем, кто хочет стать готовым к работе FrontEnd-разработчиком, я бы порекомендовал попробовать курс от ooloo.io. Он знакомит с такими концепциями, как создание идеального дизайна, планирование и реализация сложного компонента пользовательского интерфейса, отладка внутри IDE и Написание интеграционных тестов, которые не требуются в большинстве онлайн-руководств или курсов. И да, я получил много вдохновения от этого курса, который в конечном итоге помог мне написать эту статью.

Эта статья изначально размещена на dev.to