Простое руководство по тестированию взаимодействия с пользователем с помощью библиотеки тестирования React

RedDeveloper
29.01.2023 12:32
Простое руководство по тестированию взаимодействия с пользователем с помощью библиотеки тестирования React

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

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

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

Настройка

Приложение с помощью команды create-react-app testing-user-interactions. Далее установите библиотеку user-event.

npm i user-event

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

Каждого теста вам нужно будет выполнить следующие импорты.

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'

Кроме того, импортируйте компонент, который вы будете тестировать.

Как протестировать взаимодействие элементов выбора?

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

<select id='selectCity'>
    <option> Mumbai</option>
    <option> Bangalore</option>
    <option> Chennai</option>
</select>

Рассмотрим, как сделать запрос к элементу. Вы можете сделать screen.getByRole('combobox'). См. список ролей здесь .

Если есть несколько элементов select, вы можете использовать getByLabelText(). Для этого добавьте метку, <label htmlFor="selectCity">Select City</label> для элемента и выполните screen.getByLabelText(/select city/i). Вы получите элемент.

Используйте метод selectOptions() библиотеки для имитации выбора пользователем опции. Он принимает три аргумента: элемент, значение и любые опции.

userEvent.selectOptions(
    screen.getByRole('combobox'),
    'Chennai'
)

Затем сделайте утверждение.

expect(screen.getByRole('combobox')).toHaveValue('Chennai')

Поскольку пользователь выбрал Chennai, наш элемент select имеет то же значение, поэтому тест пройден.

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

Я сделаю обновление состояния при изменении значения элемента select. Сначала у меня будет состояние cityName для отображения выбранного варианта.

const [cityName, setCityName] = useState('Mumbai');
<h2> City Name:  {cityName} </h2>

Затем я добавлю атрибут onChange с методом, который обновляет состояние.

onChange = {(e) => { setCityName(e.target.value)}}

Чтобы написать тест для этого, следуйте тому же процессу.

test("City Name header rendering", () => {
    render(<SelectElement/>)
    userEvent.selectOptions(
        screen.getByRole('combobox'),
        'Bangalore'
    )
    expect(screen.getByRole('heading', 
              { name: /bangalore/i})).toBeInTheDocument();
})  

В конце сделайте утверждение, запросив элемент h2 с помощью getByRole() и параметра name.

Событие наведения

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

<div className="tooltip">
    <p> Hover over me</p>
    <span className="tooltiptext">Tooltip text</span>
</div>

Есть также некоторые CSS стили для классов, определенных выше, но я не буду показывать их здесь. В принципе, когда пользователь наводит курсор на текст Hover over me, текст всплывающей подсказки становится видимым.

Чтобы имитировать наведение пользователя на элемент, используйте метод hover() и сделайте утверждение.

test("Tooltip visible", () => {
    render(<HoverEvent/>)
    userEvent.hover(screen.getByText(/hover over me/i))
    expect(screen.getByText(/tooltip text/i)).toBeInTheDocument()
})

Это все, что вам нужно сделать.

Тестирование события загрузки файлов

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

Пусть у нас есть вход типа file.

<label htmlFor="singleFile">Upload File</label>
<input type='file' id='singleFile' />

Теста, вам нужно будет имитировать загрузку файла пользователем. Для этого используйте метод upload().

test("Test single file upload", () => {
    render(<FileUpload/>)
    const file = new File(['hello'], 'hello.png', {type: 'image/png'})
    userEvent.upload(screen.getByLabelText(/upload file/i), file)
    expect(screen.getByLabelText(/upload file/i).files[0]).toEqual(file);
})

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

Теперь давайте рассмотрим пример с загрузкой нескольких файлов.

<label htmlFor='multipleFiles'>Upload multiple files</label>
<input type='file' id='multipleFiles' multiple />

В нашем тесте загрузите несколько файлов.

test("Test multiple file uploads", () => {
    render(<FileUpload/>)
    const files = [
        new File(['file1'], 'file1.png', {type: 'image/png'}),
        new File(['file2'], 'file2.png', {type: 'application/pdf'})
    ]
    userEvent.upload(screen.getByLabelText(/upload multiple files/i), files)
    const fileInput = screen.getByLabelText(/upload multiple files/i)
    expect(fileInput.files.length).toBe(2);
    expect(fileInput.files[0]).toEqual(files[0]);
    expect(fileInput.files[1]).toEqual(files[1]);
})

Утверждения, проверить количество файлов и соответствие обоих файлов загруженным. Таким образом, все наши тесты проходят со 100% покрытием.

Теперь время для испытания. Просмотрите мои посты Muliple File Uploads и Different Examples of the useState Hook и напишите модульные тесты для всех функций. Загрузите свой код на GitHub и поделитесь им со мной.

Поведение формы

Важной особенностью большинства веб-приложений является работа с формами. При написании тестов для React-приложения вам, скорее всего, понадобится проверить поведение компонента с формами.

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

const [name, setName] = useState('')
const [password, setPassword] = useState('')
const [country, setCountry] = useState('')
const [formSubmitted, setFormSubmitted] = useState(false)
<form>
    <input onChange = {(e) => {setName(e.target.value)}} 
            placeholder='Enter name'/>
    <input onChange = {(e) => {setPassword(e.target.value)}} 
            placeholder='Enter password'/>
    <label htmlFor='selectCountry'> Select Country </label>
    <select id='selectCountry' 
            onChange = {(e) => {setCountry(e.target.value)}}>
        <option>India</option>
        <option>Japan</option>
        <option>China</option>
        <option>USA</option>
        <option>England</option>
    </select>
    <button onClick = {handleSubmit}> Submit </button>
</form>

В атрибуте onChange выполните обновление состояний. В конце выведите текст об успешном отправлении формы.

{formSubmitted && 'Form Submitted'}

Давайте добавим некоторую валидацию в метод handleSubmit. Когда валидация не пройдет, будет показан список ошибок. Создайте еще одно состояние для хранения ошибок.

const [errors, setErrors] = useState([])

Теперь реализуйте метод handleSubmit.

const handleSubmit = (event) => {
    event.preventDefault();
    const errorList=[];
    if (name == '' || password == '' || country == '') {
        errorList.push('Please fill all the details')
    }
    
    const regex = /^[a-z]*$/i;
    if (!name.match(regex)) {
        errorList.push('Please enter a valid name')
    }

    if (errorList.length != 0) {
        setErrors(errorList);
        setFormSubmitted(false);
        return;  
    } 
    setErrors([]);
    setFormSubmitted(true)
}

Сначала мы выполним event.preventDefault(), который отменит все события, препятствующие отправке формы. Затем мы добавим несколько проверок на ошибки и добавим все ошибки в состояние errors. В конце, если ошибок нет, установим errors в пустой массив и установим formSubmitted как true.

Мы также выведем список всех ошибок, который будет виден при нажатии на кнопку submit.

errors.map(err => (
    <p>
        {err}
    </p>
))

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

test("Form getting submitted with correct input values", () => {
    render(<FormBehaviour/>)

    userEvent.type(screen.getByPlaceholderText(/enter name/i), 'kunal')
    userEvent.type(screen.getByPlaceholderText(/enter password/i), 'pass')
    userEvent.selectOptions(screen.getByLabelText(/select country/i), 'India')
    userEvent.click(screen.getByText(/submit/i))

    expect(screen.getByText(/form submitted/i)).toBeInTheDocument();
})

Методы type и click библиотеки userEvent для имитации соответствующих действий. В конце добавьте утверждение, чтобы проверить, виден ли текст, соответствующий regex /form submitted/i, в документе, поскольку наша логика отображает текст, как только форма отправлена.

методы type и click библиотеки userEvent для имитации соответствующих действий В конце

Тест пройден, но все еще есть несколько незакрытых строк. Мы протестировали поведение нормальной формы. Однако у нас также есть механизмы обработки ошибок. Поэтому вам также необходимо написать тесты, которые охватывают различные сценарии ошибок.

Сначала имитируйте, что пользователь нажимает кнопку отправки, ничего не вводя.

test("Empty fields error shown", () => {
    render(<FormBehaviour/>)
    userEvent.click(screen.getByText(/submit/i))
    expect(screen.getByText(/please fill all the details/i))
            .toBeInTheDocument()
}) 

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

Еще не все строчки рассмотрены. У нас остался еще один сценарий ошибки. Что если пользователь введет неправильное имя?

test("Invalid name error shown", () => {
    render(<FormBehaviour/>)
    userEvent.type(screen.getByPlaceholderText(/enter name/i), '@###')
    userEvent.type(screen.getByPlaceholderText(/enter password/i), 'pass')
    userEvent.selectOptions(screen.getByLabelText(/select country/i), 'India')
    userEvent.click(screen.getByText(/submit/i))
    expect(screen.getByText(/Please enter a valid name/i)).toBeInTheDocument();
})

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

Этот тест похож на первый тест только здесь пользователь вводит недопустимое имя Поэтому

Теперь все наши тесты проходят со 100% покрытием.

Вы можете найти полный код на GitHub .

Заключение

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

Я также продемонстрировал тестирование поведения формы, охватывающее все возможные сценарии. Надеюсь, вы поняли, как использовать библиотеку userEvent для тестирования поведения вашего компонента с пользовательскими событиями.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

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

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

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