-
Notifications
You must be signed in to change notification settings - Fork 0
Frontend Coverage
Подход к разработке, который заключается в последовательном написании теста, который будет преднамеренно провален, после чего пишется код, который удовлетворит тест.
Следующий этап – добавление нового функционала для тестирования или усложнение существующего. Тест проваливается. После чего пишется, код, который удовлетворит уже новый тест.
Процесс разработки с использованием этого подхода можно описать как цикл: тестирование – провал – написание рабочего кода – усложнение теста.
- вёрстка - например: кнопки, инпаты, фрагменты, страницы
- функционал - например: вызов модального окна, переход на роут
- бизнес-логика - например: переход по пользовательскому маршруту (CJM) с соблюдением условий (conditions)
- тестирование изолированных модулей, например:
ui
,hook
- это тестирование фундаментальных частей системы
- тестирования группы модулей, например:
fragments
,pages
- это тестирование позволяет выявить и предотвратить возможные конфликты при использовании нескольких модулей
- тестирование процесса или сценария использования приложения, рассмотрим на примере страницы регистрации:
- переход на страницу
- ввод логина
- получение реакции (валидация)
- ввод пароля
- получение реакции (валидация)
- нажатие на кнопку регистрации
- получение реакции (сообщение)
- если первый тест пройден - проверяется следующий этап, например следующий этап регистрации - верификация
Можно указывать название тестируемого файла, или --watch
-
@atls/config-jest
- наш конфигjest
-
@testing-library/react
- библиотека для тестирования приложений наReact
-
@emotion/jest
- библиотека для тестирования UI написанного с помощьюemotion
module.exports = {
compiler: {
reactRemoveProperties: true
}
}
Благодаря этому в дев и билд не попадут аттрибуты data-testid
, которые нам могут понадобиться для тестов сложных компонентов/фрагментов.
import React from 'react'
import { FC } from 'react'
import { memo } from 'react'
import { useMemo } from 'react'
import { Box } from '@ui/layout'
import { ProgressElement } from './progress.element'
import { ProgressProps } from './progress.interfaces'
export const Progress: FC<ProgressProps> = memo(({ percent, color = 'pink' }) => {
const currentPercent = useMemo(() => {
if (percent < 0) return 0
if (percent > 100) return 100
return percent
}, [percent])
return (
<Box
position='relative'
width='100%'
height={3}
borderRadius='f2'
backgroundColor='gray'
flexShrink={0}
>
<ProgressElement
position='absolute'
top={0}
left={0}
height={3}
width={`${currentPercent}%`}
backgroundColor={color}
borderRadius='f2'
/>
</Box>
)
})
- Вёрстка - убедиться что контейнер с потомками успешно отрендерился
- Функционал - при передаче значения
percent
в пропсах - отображает нужный нам прогресс
// Здесь мы обозначаем среду для `jest`, в которой будет выполняться тест.
// Для фронтенда это всегда будет `jsdom`
/**
* @jest-environment jsdom
*/
import { RenderResult } from '@testing-library/react'
// Расширение для правил тестирования, чтобы можно было задействовать `toHaveStyleRule`
import { matchers } from '@emotion/jest'
import { render } from '@testing-library/react'
import React from 'react'
// Берем провайдер темы и объект темы с локального пакета - для правильной работы `styled-components`
// необходимо обязательно рендерить тему
import { ThemeProvider } from '@ui/theme'
import { theme } from '@ui/theme'
// Сам тестируемый компонент
import { Progress } from './progress.component'
// Расширяем мэтчеры тестирования с помощью `@emotion/jest`
expect.extend(matchers)
type CustomRender = (element: React.ReactNode | React.ReactNode[]) => RenderResult
// Создаем кастомный рендер - чтобы не повторятся в наших тестах с рендером темы
const customRender: CustomRender = (element) => render(<ThemeProvider>{element}</ThemeProvider>)
// С `describe` начинается группа тестирования. С его помощью обозначаются группы тестов,
// которые можно логически объединить в одну область тестирования. Можно обходиться и без этого,
// но тогда тесты будут разрозненны и тяжело читаемы в результате тестов.
describe('Progress component', () => {
// Это уже наш тест. Описываем что тестируем и передаем фукнцию с тестом.
// К `it` можно добавлять "модификаторы". Напр.:
// - `it.only` - из всех тестов в файле будет выполнятся только этот
// - `it.skip` - этот тест будет пропускаться
it('renders correct percent width', () => {
const testPercent = 50
// Рендерим наш компонент.
const { container } = customRender(<Progress percent={testPercent} />)
// Т.к. наш компонент не имеет каких либо кнопок, инпутов или текста, то
// поиск его в дереве происходит стандартным `querySelector`.
const progressBar = container.querySelector('div > div > div')
// Проверка что такой элемент в дереве нашелся
expect(progressBar).toBeTruthy()
// Проверка что у элемента есть правило ширины и оно соответствует значению выше.
expect(progressBar).toHaveStyleRule('width', `${testPercent}%`)
})
it('applies the correct color', () => {
const testColor = 'pink'
const { container } = customRender(<Progress percent={50} color={testColor} />)
const progressBar = container.querySelector('div > div > div')
// Проверяем что наш компонент при заданных пропсах имеет в стилях значение из темы.
expect(progressBar).toHaveStyleRule('background-color', theme.colors[testColor])
})
})
Компонент рендерится при клике по элементу, к которому прикреплен:
- Верстка
- Функционал - рендер по триггеру (клик, ховер)
/**
* @jest-environment jsdom
*/
import { RenderResult } from '@testing-library/react'
import { fireEvent } from '@testing-library/react'
import { act } from '@testing-library/react'
import { render } from '@testing-library/react'
import React from 'react'
import { ThemeProvider } from '@ui/theme'
import { Tooltip } from './tooltip.component'
type CustomRender = (element: React.ReactNode | React.ReactNode[]) => RenderResult
// Наш тултип зависит от размеров экрана, т.е. работает с `ResizeObserver`.
// Мы рендерим наши компоненты с помощью `jest`, а значит не в браузере.
// Поэтому приходится "мокать" этот функционал браузера.
// "Мокать" == заменять что-то реальное "фейком"
global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
}))
const customRender: CustomRender = (element) => render(<ThemeProvider>{element}</ThemeProvider>)
describe('Tooltip component', () => {
it('renders with text', async () => {
const { findByText, getByText } = customRender(
<Tooltip trigger='hover' container={<span>Tooltip text</span>}>
<span>Click me</span>
</Tooltip>
)
const triggerSpan = getByText('Click me')
expect(triggerSpan).toBeTruthy()
// Здесь мы запускаем эвент по клику на элемент с текстом.
// Таким образом триггерим рендер тултипа.
act(() => fireEvent.click(triggerSpan))
const tooltipText = await findByText('Tooltip text')
expect(tooltipText).toBeTruthy()
})
})
Мы должны писать тесты так, будто на страницу пришёл пользователь и проводит действия. Когда мы оказываемся на любой странице, за что наш глаз цепляется? Это текст, кнопки, инпуты, формы... Все что "осязаемо" для глаза.
Вот как тестировать форму с точки зрения пользователя:
- попал на страницу
- увидел форму
- увидел инпут
- заполнил инпут
- увидел еще инпут
- заполнил инпут
- ...
- увидел кнопку для сабмита
- нажал кнопку
Для симуляции поведения пользователя рекомендуется искать элементы так (оригинал). Нумерация в порядке приоритета:
- Запросы, доступные для всех: Запросы, которые отражают опыт использования визуальными/мышевыми пользователями, а также теми, кто использует вспомогательные технологии.
-
getByRole
: Этот метод можно использовать для запроса любого элемента, который представлен в дереве доступности. С опциейname
вы можете фильтровать возвращаемые элементы по их доступному имени. Это должно быть вашим главным приоритетом почти во всём. Нет многого, что вы не можете получить с его помощью (если не можете, возможно, ваш UI недоступен). Чаще всего это будет использоваться с опциейname
следующим образом:getByRole('button', {name: /submit/i})
. Проверьте список ролей. -
getByLabelText
: Этот метод действительно хорош для полей форм. Перемещаясь по форме веб-сайта, пользователи находят элементы, используя текст метки. Этот метод эмулирует это поведение, поэтому он должен быть вашим главным приоритетом. -
getByPlaceholderText
: Заполнитель не является заменой метки. Но если это всё, что у вас есть, то это лучше, чем альтернативы. -
getByText
: Вне форм текстовое содержимое является основным способом, с помощью которого пользователи находят элементы. Этот метод можно использовать для поиска неинтерактивных элементов (таких какdiv
,span
и параграфы). -
getByDisplayValue
: Текущее значение элемента формы может быть полезно при навигации по странице с заполненными значениями.
- Семантические запросы: Селекторы, соответствующие HTML5 и ARIA. Обратите внимание, что пользовательский опыт взаимодействия с этими атрибутами сильно различается в разных браузерах и вспомогательных технологиях.
-
getByAltText
: Если ваш элемент поддерживает альтернативный текст (img
,area
,input
и любой пользовательский элемент), то вы можете использовать это для поиска такого элемента. -
getByTitle
: Атрибутtitle
не последовательно читается скринридерами и не виден по умолчанию для пользователей с видением.
- Тестовые идентификаторы
-
getByTestId
: Пользователь не может видеть (или слышать) эти, поэтому это рекомендуется только в случаях, когда вы не можете сопоставить по роли или тексту, или это не имеет смысла (например, текст динамичен).
Шпаргалка запросов
Многие методы дают доступ к поиску по тексту (текст, плейсхолдер, вэлью в инпутах...) либо уточняющий поиск (...ByRole(..., { name: '...' })
). В этих методах нельзя искать по локали/переводу. Допустимые методы поиска:
-
aria-label
: лучшие практики -
testid
: см. далее
Этим пользуемся в самом крайнем случае. Правила по его неймингу:
...data-testid='component-subcomponent-subsubcomponent-...-id'
Например, для свитча, который:
- имеет в детях нужный нам
<span>...</span>
- которого может быть много на одном фрагменте/странице и нужно обратиться к конкретному
<Switch testId={1} />
<span data-testid='switch-span-{props.testId}'>...</span>
Если необходимо добавить уникальный testid
через пропсы, то добавляем в конец как ID.
https://gist.github.com/Nelfimov/ba3768ecf31f804b7f9378807be4c2fa - data-testid
https://gist.github.com/Nelfimov/ade5263ea89d82a240368fe7aa8c09ba - хук
https://gist.github.com/Nelfimov/aa79af1bb2cff9af4e20963f4ccb7cea - фрагмент с клик эвентами
https://gist.github.com/Nelfimov/b7700fa608ca696dc6c9f708e624b1dd - компонент с копированием в clipboard
по клику
будет дополняться