Как тестировать React-компоненты? Какую библиотеку использовать? Как тестировать компоненты, которые берут данные из Redux, а не из пропсов? Как тестировать компоненты, в которых используется роутинг с помощью React-router-dom? Что делать, если в компоненте есть асинхронный код?

Привет, Хабр, меня зовут Даниил, я выпускник Elbrus Bootcamp. Это вопросы в моей голове, когда на работе меня впервые попросили покрыть тестами компонент. Я, разумеется, стал гуглить тестирование React-компонентов в связке с Redux и React-router-dom, и понял, что в сети есть много ответов на вопрос, зачем нужно тестирование, но мало кто объясняет, как написать тесты. А если и объясняет, то в  общих чертах на абстрактных примерах. Мне не хватало статьи, вооружившись которой, начинающий разработчик мог бы выполнить тест на реальном продукте. Поэтому я решил написать ее сам.

Статья предназначена для таких же, как я: разработчиков, которые пришли на свою первую работу и впервые столкнулись с необходимостью написать тесты. Более опытных коллег прошу проверить мои выводы, дать советы и замечания.

React-testing-library

Для наглядности я написал небольшое React-приложение: меню навигации, страничка с приветствием, на страничке Send форма отправки денег с одного кошелька на другой, с возможностью менять кошельки, вводить сумму перевода с расчетом комиссии, с выводом ряда ошибок. После успешной отправки средств пользователь попадает на экран успеха.

Для этой статьи я выбрал библиотеку react-testing-library, а все примеры кода будут написаны на TypeScript. React-testing-library — это фреймворк огромной библиотеки Testing Library, которая для React по умолчанию использует в своей основе Jest. Все что будет описано ниже, относится именно к этой библиотеке. Небольшая информация по синтаксису:

import { render, screen } from '@testing-library/react';
import Send from './index';
 
test('From element: exist in the DOM', () => {
  render(<Send />)
  expect(screen.getByLabelText<HTMLSelectElement>('From')).toBeInTheDocument();
});

Тест — это функция, которая принимает два аргумента: название теста и callback-функцию с логикой.

Внутри callback необходимо вызвать функцию render, которая импортируется из библиотеки и принимает тестируемый React-компонент.

Дальше идет главная конструкция для теста — функция expect, которую можно описать так:

expect(<реальное состояние>).toBe(<ожидаемое состояние>);

В данном случае нам необходимо убедиться, что выпадающее меню с кошельками <реальное состояние> было отрисовано в HTML-разметке <ожидаемое состояние>.

Чтобы получить этот элемент разметки, необходимо воспользоваться методами объекта screen, который мы также импортировали из библиотеки. Он содержит различные методы для взаимодействия с DOM-деревом. В данном случае мы вызываем метод «getByLabelText», который осуществляет поиск элемента, который ассоциирован с тегом <label>, в котором есть текст «From».

Выполнив поиск элемента, дописываем логическое выражение. В данном случае вызовом функции .toBeInTheDocument().

Литературный смысл теста можно описать так: «после рендера компонента на странице отображается элемент с лейблом “From”».

Ниже будет более детальная информация по поисковым методам объекта screen и их отличиям.

Работа с DOM-деревом

У react-testing-library есть две ключевые особенности. Во-первых, она позволяет абстрагироваться от логики внутри компонента. Во-вторых, отслеживает состояние элементов реального DOM. Это означает, что библиотека старается максимально имитировать пользовательское поведение и использовать только то, что может видеть он. 

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

А что насчет пункта с абстрагированием от логики внутри компонента? Есть функция расчета комиссии за перевод. Мы можем предположить, что расчет комиссии содержит логику, НО с данной библиотекой мы можем проверить только то, что после расчёта комиссии в DOM-дереве произошли изменения, которые мы ожидаем. То есть мы не проверяем, что state компонента изменился, как делали бы, используя другие библиотеки, а смотрим, что сумма комиссии на страничке — то, что видит пользователь — соответствует нашим ожиданиям. Внутри компонента логика может быть любой.

Мы знаем, что комиссия равна 1% от суммы перевода:

import { render, screen } from '@testing-library/react';
import Send from './index';
 
test('Fee element: value was changed after change of amount value', () => {
  render(<Send />)
  userEvent.type(screen.getByLabelText<HTMLInputElement>('Amount'), '1')
  expect(screen.getByLabelText<HTMLInputElement>('Fee').value).toBe('0.01');
})

Литературное описание теста: «после того, как пользователь ввел значение “1” в поле ввода с лейблом “Amount”, поле ввода с лейблом “Fee” будет равно 0.01». 

В примере выше использовался еще один важный объект, предоставляемый библиотекой React Testing Library — userEvent. Он предоставляет набор методов для взаимодействия с элементами DOM, которые максимально приближены к реальному поведению пользователя.

 Он может что-то напечатать:

userEvent.type(<Элемент с которым взаимодействует юзер>, «текст который он вводит») 

может нажать на кнопку:

userEvent.click(<Элемент с которым взаимодействует юзер>)

и так далее.

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

Поиск элемента

Теперь, когда основная концепция описана, углубимся в синтаксис, особенности и тонкости. 

Поисковые методы по DOM-дереву, которые предоставляет screen, делятся на три категории: 

1. getBy — поиск элемента на странице;

2. queryBy — поиск элемента, которого нет на странице;

3. findBy — поиск элемента на странице, который зависит от асинхронного кода.

Если getBy не вызывает вопросов и используется чаще всего, то две другие категории требуют внимания. Начнем с queryBy. В нашем примере есть валидация операции отправки денег. Если валидация не прошла, пользователю выводится ошибка:

{error.status && (
  <div className="error">
    {error.message}
  </div>
)}

Значит, когда пользователь только зашел на страницу с нашим компонентом, он не должен видеть ошибку. Чтобы это проверить, мы должны использовать queryBy:

test('Error element: not to be in the component by default', async ()=> {
  render(<Send />)
  expect(screen.queryByText('Error')).not.toBeInTheDocument();
})

Также стоит обратить внимание, что для любого утверждения после expect не существует обратного утверждения (например, NotToBeInTheDocument). Все отрицания выполняются с помощью конструкции .not перед выражением. Если вы попробуете выполнить поиск, используя getBy, то получите ошибку в синтаксисе теста — он не найдет элемент.

Следующая категория — findBy для работы с асинхронным кодом. Загрузка имени юзера происходит асинхронно:

const getUser = () => Promise.resolve({ id: '1', name: 'John Doe' })

А отрисовка элемента происходит после получения данных о юзере:

{user && <div>Hello {user.name}</div>}

Значит, тут необходимо использовать findBy:

test('User element: exist after fetch data', async function () {

  render(<Send />)

  expect(await screen.findByText(/Hello/)).toBeInTheDocument()

});

Обратите внимание, что если нам необходимо дождаться выполнения асинхронной функции, то и callback-функция, передаваемая в тест, должна быть асинхронной.

Вот удобная таблица методов поиска:

getBy

queryBy

findBy

getByText

queryByText

findByText

getByRole

queryByRole

findByRole

getByLabelText

queryByLabelText

findByLabelText

getByPlaceholderText

queryByPlaceholderText

findByPlaceholderText

getByAltText

queryByAltText

findByAltText

getByDisplayValue

queryByDisplayValue

findByDisplayValue

getByTestId

queryByTestId

findByTestId

getByTitle

queryByTitle

findByTitle

Подробно хочется остановится только на двух:

  1. Поиск по роли (getByRole, queryByRole, findByRole). Помогает найти элемент по его логической роли в документе. Вот некоторые роли:

‘combobox’ - тег <select />

‘button’ - тег <button /> или <input type=«submit» />

В нашем примере для поиска кнопки отправки используется getByRole:

userEvent.click(screen.getByRole('button'));     
  1. Поиск по testId. В коде вы можете указать любому компоненту data-атрибут testId и использовать это значение при поиске элемента:

<div data-testid="custom-element" />

У всех методов поиска есть варианты поиска всех элементов, соответствующих условию на странице:

  • getAllByText

  • queryAllByPlaceholderText

  • findAllByTestId

  • и т.д. для всех поисковых методов

Логические выражения

Следует уделить внимание и правильному использованию логических выражений. Они проверяют выполнение теста:

expect(<реальное состояние>).toBe(<ожидаемое состояние>);

Приведу пару примеров использования разных методов функции expect:

  • toBeNull:

expect(screen.queryByText(/Hello/)).toBeNull();

ожидание, что компонент будет равен null

  • toBeInTheDocument:

expect(await screen.findByText(/Hello/)).toBeInTheDocument()

ожидание, что компонент есть в DOM

  • toBeTruthy:

expect(screen.getByLabelText<HTMLInputElement>('Fee').disabled).toBeTruthy();

ожидание, что поле ввода с лейблом "Fee" будет недоступно для пользовательского ввода

  • toHaveTextContent:

expect(screen.getByText<HTMLSpanElement>(/balance/i)).toHaveTextContent('10');

ожидание, что в текстовом элементе, который содержит текст "balance", будет текст '10' 

  • toBe:

expect(state[0].balance).toBe(4.95);

ожидание, что одна цифра будет равна другой цифре (в данном случае баланс кошелька из state будет равен 4.95)

Библиотека построена на базе Jest, поэтому все методы с детальным описанием можно увидеть на официальной странице jest.

В чем преимущество использования максимально подходящих под ситуацию утверждений, хотя всегда есть желание поставить .toBe() и не заморачиваться? 

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

userEvent и fireEvent

Как я уже говорил, userEvent имитирует поведение пользователя. В большинстве ситуаций вы будете использовать его. Вот полный список методов:

  • ​​click - нажатие на элемент;

  • dblClick - двойное нажатие на элемент;

  • type - печать текста;

  • clear - очистить поле ввода (только <input/> и <select/>);

  • tab - нажатие на tab;

  • hover - наведение мышки;

  • unhover - снятие наведения мышки;

  • upload - загрузка файла;

  • selectOptions - выбрать из выпадающего списка (<select />);

  • deselectOptions -убрать выбор ;

  • paste - вставить из буфера обмена;

  • keyboard - имитация нажатия клавиш;

Ссылка на официальную документацию.

Философия react-testing-library учит нас, что необходимо стремиться использовать userEvent как можно чаще для имитации пользовательского поведения. Но иногда возможностей userEvent не хватает. Тогда можно обратиться к fireEvent. По факту, userEvent — это оболочка над fireEvent, которая позволяет получить более высокий уровень абстракции. А вот fireEvent — это имитация поведения DOM-элементов. 

Возьмем метод type у userEvent и сравним с тем же событием в fireEvent:

  • userEvent:

userEvent.type(screen.getByLabelText<HTMLInputElement>('Amount'), '10')

  • fireEvent:

fireEvent.change(screen.getByLabelText<HTMLInputElement>('Amount'), {target: { value: '10' }});

fireEvent принимает два аргумента: элемент DOM-дерева и объект события, которое с ним происходит.  fireEvent содержит много методов —  "keyPress", "focusOut", "drag" — полный список я смог найти только в их типах

Redux

Как тестировать компонент, если он берет данные из Redux-стора? Нужно сделать настоящий store, только внутрь передать заранее написанные данные:

Функция renderWithRedux принимает в себя компонент, который нужно отрисовать, и данные, с которыми этот элемент будет отрисован. Внутри себя она создает store и оборачивает компонент тегом <Provider/>, куда и передает созданный store. Обратите внимание: функция возвращает не только рендер компонента, но и сам store.

mport { createStore } from "redux";
import { Provider } from "react-redux";
import { reducer, WalletItem } from "../../redux/store";
 
const renderWithRedux = (
  component: JSX.Element,
  { initialState,
    store = createStore(reducer, initialState)
  }: {initialState?: WalletItem[]; store?: any } = {}
) => {
  return {
    ...render((
        <Provider store={store}>
          {component}
        </Provider>,
    store
  }
}

Разберем на конкретных примерах.

test('From element: default value is the first wallet', function () {
const { store } = renderWithRedux(<Send />, { initialState: MOCK_WALLET_LIST})
const state = store.getState();
expect(screen.getByLabelText<HTMLSelectElement>('From').value).toBe(state[0].id.toString());
});

Литературное описание теста: «Значение поля ввода с лейблом "From" по умолчанию будет равно "id" первого кошелька».

В функцию renderWithRedux передается тестируемый компонент и данные, а возвращает функция store. Это обычный стор Redux, из него мы получаем state и можем отслеживать, как он меняется в результате различных действий. Например:

test('SendTransaction: balances were changed after sending funds',  () => {
  const { store } = renderWithRedux(<Send />, { initialState: MOCK_WALLET_LIST})
  const state = store.getState();
  userEvent.type(screen.getByLabelText<HTMLInputElement>('Amount'), '5')
  userEvent.click(screen.getByRole('button'));
  expect(state[0].balance).toBe(4.95);
  expect(state[1].balance).toBe(12);
})

Литературное описание теста: «После того, как пользователь ввел в поле Amount строку "5" и нажал на кнопку, выполняется отправка денег, в результате которой баланс одного кошелька будет равен "4.95", а другого — "12"». 

ВАЖНО! Все тесты работают с одним стором. Если вы в первом тесте изменили стор (например, для проверки отправили деньги с одного кошелька на другой), то во всех последующих тестах баланс кошельков будет отличаться от исходного.

Функция renderWithRedux может быть написана вами по-другому. Реализация не важна, главное, чтобы она оборачивала компонент провайдером со стором. Однако реализация в этой статье написана не мной и применяется повсеместно. Своего рода стандарт. Надеюсь, в какой-то момент она станет частью библиотеки.

Router

Имитировать роутинг вам потребуется не только для проверки адреса, куда переходит пользователь, но и если используете navigate в компоненте:

const navigate = useNavigate();
…
navigate('/success');

Для имитации роутинга нужно обернуть компонент в <MemoryRouter/>. Это специальный компонент, который помогает работать с роутингом вне браузера, в том числе для тестирования. Он хранит историю "URL". При этом не пишет в адресную строку и не читает из нее, так как в тестах никакой адресной строки нет.

const renderWithRouter = (
  component: JSX.Element ) => (
    render((
      <MemoryRouter>
          {component}
      </MemoryRouter>))
)

 В нашем примере есть три роута:

  • "/" — страница "Welcome";

  • "/send" — страница "Send";

  • "/success" — страница "Success";

Для тестирования этого примера нужно выполнить рендер компонентов не только с роутингом, но и с Redux. Так выглядит функция renderWithReduxAndRouter:

const renderWithReduxAndRouter = (
  component: JSX.Element,
  { initialState,
    store = createStore(reducer, initialState)
  }: {initialState?: WalletItem[]; store?: any } = {}
) => {
  return {
    ...render((
      <MemoryRouter>
        <Provider store={store}>
          {component}
        </Provider>
      </MemoryRouter>)),
    store
  }
}

Компонент, обеспечивающий роутинг, в примере называется <NavBar/> и выглядит так:

import React from 'react';
import {Link, Route, Routes} from "react-router-dom";
import Send from "./send";
import Success from "./success";
import Welcome from "./welcome";
 
function NavBar() {
  return (
    <div>
      <nav className="navbar">
        <Link className="navbarLink" to='/'>Welcome</Link>
        <Link className="navbarLink" to='/send'>Send</Link>
      </nav>
      <Routes>
        <Route path={'/'} element={<Welcome/>}/>
        <Route path={'/send'} element={<Send/>}/>
        <Route path='/success' element={<Success/>}/>
      </Routes>
    </div>
  );
}
 
export default NavBar;

Тесты, которые проверяют все три роута:

test('First element is Welcome page', () => {
  renderWithReduxAndRouter(<NavBar/>, { initialState: MOCK_WALLET_LIST })
  expect(screen.getByText('Welcome!')).toBeInTheDocument();
})

Литературное описание теста: "Первая страница, которую видит юзер — страница приветствия".

test('After clicking the "Send" link, the "Send" page opens.', () => {
  renderWithReduxAndRouter(<NavBar/>, { initialState: MOCK_WALLET_LIST })
  userEvent.click(screen.getByText('Send'));
  expect(screen.getByRole('button')).toBeInTheDocument()
})

Литературное описание теста: "После нажатия на ссылку "Send" пользователь переходит на страницу отправки средств".

 test('After clicking the "Confirm" button, the "Success" page opens.', () => {
  const { store } = renderWithReduxAndRouter(<NavBar/>, { initialState: MOCK_WALLET_LIST })
  const state = store.getState();
  userEvent.click(screen.getByText('Send'));
  userEvent.selectOptions(screen.getByLabelText<HTMLSelectElement>('From'), state[3].id.toString())
  userEvent.selectOptions(screen.getByLabelText<HTMLSelectElement>('To'), state[4].id.toString())
  userEvent.type(screen.getByLabelText('Amount'), '1');
  userEvent.click(screen.getByRole('button'));
  expect(screen.getByTestId('success')).toBeInTheDocument()
})

Литературное описание теста: "После отправки средств пользователь переходит на страницу успеха".

На последнем примере можно увидеть, как перед отправкой были выбраны кошельки 4 и 5. Все потому, что у нас уже есть тест, который изменяет балансы кошельков. А стор у нас единый для всех тестов. Значит, нужно использовать балансы кошельков, которые до этого не участвовали в тестах, либо учитывать эти изменения в других тестах.

Заключение

Писать тесты — не то же самое, что писать код. Главное отличие состоит в необходимости имитировать различные сущности: роутер, пользовательское поведение, данные, библиотеки. Именно в этой области и возникают основные сложности при написании тестов.

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

В свой первый релиз я сделал одно из полей в Redux большими буквами, а не маленькими, как было до этого. В результате сломал огромное количество функционала, который был чувствителен к регистру. Привет, срочный hotfix на следующий за релизом день. В этот день на вопрос о необходимости тестирования я себе ответил.

Полезные ссылки

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

- текст на английском языке 

- видео на русском языке

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

Комментарии (12)


  1. Mox
    11.02.2022 18:00
    +3

    Я периодически думаю можно ли делать интеграционные тесты, но на не моковых ответах API.

    Цель такая - быть уверенным что все не отвалится не только на "правильных" ответах API, а на тех что есть сейчас.


  1. JustDont
    11.02.2022 18:33
    +10

    Юнит-тесты реактовских компонент — невероятно бессмысленная вещь. Очень недалеко от Enterprise FizzBuzz.


    Означает ли, что если юнит-тесты пройдут — наши компоненты будут правильно отображаться и работать? Нет, не означает. Да и принципиально не может, потому что всё, что мы проверили — это делают ли наши компоненты в замокированном DOM некоторые вещи, которые мы ожидаем. От этого и до "компонент правильно отображается и работает" — пропасть, со дна которой на тебя смотрят страшные штуки по имени CSS, browser compatibility, и всё вот это.


    Означает ли, что если юнит-тесты не пройдут — наши компоненты не будут правильно отображаться и работать? И опять же нифига: на моей практике на каждую сотню случаев отвала юнит-тестов компонентов примерно штук 5 сообщали о реальных проблемах, а остальные 95 — это "я чего-то поменял в компоненте и теперь мне надо поправить тест, чтоб не падал".


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


    1. Kelaris
      14.02.2022 10:35

      Не соглашусь с вами.

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

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


      1. JustDont
        14.02.2022 13:32

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

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


        Если же ваша "внутренняя логика" компонента не совершенно примитивна (взяли уже приготовленные данные, отобразили эти данные как есть) и не написана ради оптимизаций и решения сложных проблем DOM — то ей не место внутри компонента.


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

        Вы статью-то вообще читали? Когда сторона тестов дёргает сторону тестируемого кода через селекторы, завязывающиеся на конкретные теги, классы, или (обоже) тексты — вам неизбежно придётся писать "странные и бесполезные" тесты, которые будут ломаться так сразу, как только в компоненте поменяется хоть что-нибудь по его форме (и плевать, что по смыслу не поменяется ничего).


    1. LFFATE
      14.02.2022 10:43

      Во-первых, в контексте rtl тестирование компонентов уже не является юнит. Т.к. рендерит все потомки тестируемого компонента. Во-вторых, что плохого в покрытии веток кода тестами? В-третьих, е2 тесты все равно должны быть. То есть всегда можно выбрать другие типы тестирования не взамен, а в помощь.


      1. JustDont
        14.02.2022 13:37

        Во-первых, в контексте rtl тестирование компонентов уже не является юнит.

        Да, и поэтому нужно тащить что-то еще для мокирования компонент (типа enzyme), если уж вы вступили на эту кривую дорожку. А то еще и тесты будут не юнит, а хренпоймичто.


        Во-вторых, что плохого в покрытии веток кода тестами?

        Ровно то же плохое, что и в лопаньи пузырьков на пленке. То есть, в общем-то ничего, просто само по себе это совершенно бесполезное занятие, на выходе которого имеем только кучу попорченной пленки. А в нашем случае — гору кода, которая ничего полезного не делает, и будет дальше жрать время на поддержку, пока кто-нибудь её не выкинет из проекта нафиг.


        В-третьих, е2 тесты все равно должны быть.

        Какое отношение это всё имеет к rtl и статье? Я где-то в своем комментарии сказал, что тестов не надо вообще?


        1. LFFATE
          14.02.2022 15:35

          мокирования компонент (типа enzyme)

          При чём тут мокирование? Если у меня, условно, импорт компонента из mui, то он совершенно обычно зарезолвится в тестовом окружении.

          Ровно то же плохое, что и в лопаньи пузырьков на пленке

          Неостроумные аналогии не по делу - вот что бесполезно.

          Какое отношение это всё имеет к rtl и статье? 

          Самое прямое. Речь о тестировании.

          Когда сторона тестов дёргает сторону тестируемого кода через селекторы, завязывающиеся на конкретные теги, классы, или (обоже) тексты — вам неизбежно придётся писать "странные и бесполезные" тесты

          Вам стоит ознакомиться с философией и позиционированием rtl. С семантикой в html, с доступностью. И потом писать что-то в духе того, что тесты построены не нестабильных селекторах классов. И да, какая проблема с текстами? Если есть intl с дефолтными стрингами, которые, как правило, не меняются. А реальные переводы лежат отдельно или запрашиваются с сервера.

          Большинство описанных проблем решены несколько лет назад и активно используется очень многими разработчиками, которые по вашему мнению "лопают пузырики". Пожалуй, только в русском сообществе можно встретить настолько некомпетентное, но переполненное важностью, мнение.

          Соглашусь в одном - логику из компонентов нужно выносить.


          1. JustDont
            14.02.2022 16:06

            Если у меня, условно, импорт компонента из mui, то он совершенно обычно зарезолвится в тестовом окружении.

            Это зависит исключительно от вашего тестового окружения. Может и не зарезолвиться. Почти все тестовые обвязки умеют (и обильно практикуют) вмешиваться в "обычные резолвы".


            Неостроумные аналогии не по делу — вот что бесполезно.

            Аналогия совершенно прямая. Вы спросили "что плохого в покрытии веток кода тестами", я вам ответил. Само по себе "покрытие кода" — совершенно бессмысленное занятие, которое само по себе никак не увеличивает качество продукта, ни устойчивость к рефакторингу, ни еще что вы там захотите придумать. Всё, что делает "покрытие кода" — это позволяет вам вывести красивую циферку процента покрытия, которая сама по себе ни о чем не говорит, да и не может.


            Самое прямое. Речь о тестировании.

            Rtl никак не относится к e2e и принципиально не может.


            Пожалуй, только в русском сообществе можно встретить настолько некомпетентное, но переполненное важностью, мнение.

            Это всегда очень иронично, когда мне пишут абзац голословного текста, сводящийся к "вы ничего не понимаете в N", а сразу за ним — процитированное ^_^


            Большинство описанных проблем решены несколько лет назад и активно используется очень многими разработчиками, которые по вашему мнению "лопают пузырики".

            Так что именно решено? Ваши компоненты всегда сверстаны с идеальнейшем соответствием семантике html? Ваши классы никогда не меняются? У вас всегда есть intl с дефолтными болванками текстов? И так в любом вашем проекте?
            Если это всё надо, чтоб rtl можно было удобно пользоваться — то я, пожалуй, не буду. В моем мире такая идеальность даже не ночует обычно. А жить как-то надо.


            Соглашусь в одном — логику из компонентов нужно выносить.

            Что вы тогда собрались в них тестировать? Как они взяли кусок данных и обернули его в <span>? А это точно настолько нетривиально, что без тестов никуда?


    1. XVlady5
      14.02.2022 10:44

      Не тестируй библиотеку, тестируй свой код. Для всего скопа языков так говорят. Для мира js не так?

      PS: для критичных сценариев есть более верхнеуровневые тесты, предлагаю это не обсуждать обсуждая юниты и их убойность


      1. JustDont
        14.02.2022 13:41

        Не тестируй библиотеку, тестируй свой код. Для всего скопа языков так говорят. Для мира js не так?

        Ну так что вы протестируете, взяв реакт-компоненты и rtl?
        Море всего, на самом деле, но "своего кода" там будет совсем чуть, а проверять его поведение вы будете внутри вороха абстракций. В то время, как для презентационного слоя единственный критерий прохождения или не прохождения приемки — это чтоб оно всё появилось в реальном браузере и отработало в нем же. Работает ли оно внутри абстракций rtl — это вообще абсолютно никого не волнует.


    1. Kushin
      14.02.2022 11:02

      страшные штуки по имени CSS, browser compatibility, и всё вот это.

      Как эти проблемы решает описанный подход?

       остальные 95 — это "я чего-то поменял в компоненте и теперь мне надо поправить тест, чтоб не падал".

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

      тестируйте бизнес-логику,

      Как этому противоречат юнит тесты?


      1. JustDont
        14.02.2022 13:46

        Как эти проблемы решает описанный подход?

        Какой "описанный подход"?


        И это только стор и роутер из явных зависимостей.

        Мой "стор" выражается ключевым словом class и никаких зависимостей, не относящихся к работе с данными приложения не требует. А к чему вы тут роутер припахали — для меня вообще загадка. Роутер не относится к работе с данными.


        С накрученными по пути контекстами, кастомными хуками, редукс мидлварями и прочим, о каком-то рефакторинге будут говорить разве что шепотом.

        Если вы работаете с редаксом — мои соболезнования. Я не работаю и никому не советую этого делать.
        Контексты и хуки относятся к реакту, и к работе над данными никогда не привлекаются. Да и не должны. Если вы без реакта не можете написать модельную часть приложения (работу с данными, собссно) — проблема в вас.


        Как этому противоречат юнит тесты?

        Я где-то написал, что юнит-тесты этому противоречат?