React-native-testing-library: как протестировать useEffect с помощью act
Я использую react-native-testing-library
чтобы протестировать мой реагирующий компонент. У меня есть компонент (для целей этой публикации он был упрощен):
export const ComponentUnderTest = () => {
useEffect(() => {
__make_api_call_here_then_update_state__
}, [])
return (
<View>
__content__goes__here
</View>
)
}
Вот мой (упрощенный) component.spec.tsx
:
import { render, act } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';
test('it updates content on successful call', () => {
let root;
act(() => {
root = render(<ComponentUnderTest />); // this fails with below error message
});
expect(...);
})
Теперь, когда я запускаю этот код, я получаю такую ошибку:
Can't access .root on unmounted test renderer
Я даже не понимаю, что означает это сообщение об ошибке. Я следил за документами изreact-native-testing-library
о том, как тестировать act and useEffect
.
Любая помощь будет принята с благодарностью. Спасибо
8 ответов
Я нашел обходной путь:
import { render, waitFor } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';
test('it updates content on successful call', async () => {
const root = await waitFor(() =>
render(<ComponentUnderTest />);
);
expect(...);
})
Вы можете сделать это, используя: @ testing-library / react-native
Пример:
import { cleanup, fireEvent, render, debug, act} from '@testing-library/react-native'
afterEach(() => cleanup());
test('given correct credentials, gets response token.', async () => {
const { debug, getByPlaceholderText, getByRole } = await render(<Component/>);
await act( async () => {
const emailInput = getByPlaceholderText('Email');;
const passwordInput = getByPlaceholderText('Password');
const submitBtn = getByRole('button', {name: '/submitBtn/i'});
fireEvent.changeText(emailInput, 'email');
fireEvent.changeText(passwordInput, 'password');
fireEvent.press(submitBtn);
});
});
Также должен работать с useEffect, но я сам не тестировал. Прекрасно работает с useState.
root = render(<ComponentUnderTest />);
должно быть
root = create(<ComponentUnderTest />);
---- Полный фрагмент кода. У меня работает после изменения выше
import React, { useState, useEffect } from 'react'
import { Text, View } from 'react-native'
import { render, act } from 'react-native-testing-library'
import { create } from 'react-test-renderer'
export const ComponentUnderTest = () => {
useEffect(() => {}, [])
return (
<View>
<Text>Hello</Text>
</View>
)
}
test('it updates content on successful call', () => {
let root
act(() => {
root = create(<ComponentUnderTest />)
})
})
Следующие шаги решили мой случай:
Обновление
React
а такжеreact-test-renderer
версии до 16.9 или выше, которые поддерживаютasync
функции внутриact
(насколько мне известно, оба пакета должны быть одной и той же версии)Замена
react-native-testing-library
сrender
сreact-test-renderer
сcreate
как предложил @helloworld (Спасибо, добрый сэр, это помогло мне)Выполнение тестовой функции
async
, предшествующийact
сawait
и прохождениеasync
функция к нему
Окончательный результат выглядел примерно так:
test('it updates content on successful call', async () => {
let root
await act(async () => {
root = create(<ComponentUnderTest />)
})
})
Вам нужно использовать waitFor для ожидания завершения асинхронных запросов.
Вот обновленный фрагмент кода с объяснением:
import { render, waitFor } from 'react-native-testing-library';
import { ComponentUnderTest } from './componentundertest.tsx';
test('it updates content on successful call', async () => {
// Mocking a successful API response
yourMockApi.get.mockResolvedValue({});
// Rendering the component under test
render(<ComponentUnderTest />);
// Wait for the API call to be made
await waitFor(() => expect(yourMockApi.get).toBeCalled());
});
Объяснение:
- The
yourMockApi.get
метод имитируется, чтобы вернуть успешный ответ, используя mockResolvedValue. - The
waitFor
функция используется для ожидания, пока не будет сделан имитированный вызов API, прежде чем продолжить тест. - The
await
ключевое слово используется для ожидания завершения функции waitFor перед продолжением теста.
попробуй так
it("should render <Component/>", async () => {
await act(() => {
render(<Activate />);
});
});
Подход, который я использую для тестирования асинхронных компонентов с
useEffect
который запускает повторную визуализацию с
setState
состоит в том, чтобы настроить тестовый пример как обычно, но использовать waitFor
или
findBy
для блокировки утверждений до тех пор, пока компонент не выполнит повторную визуализацию с извлеченными данными.
Вот простой и работоспособный пример:
import React, {useEffect, useState} from "react";
import {FlatList, Text} from "react-native";
import {render} from "@testing-library/react-native";
const Posts = () => {
const [posts, setPosts] = useState(null);
useEffect(() => {
const url = "https://jsonplaceholder.typicode.com/posts";
fetch(url).then(res => res.json()).then(setPosts);
}, []);
return !posts ? <Text>loading</Text> : <FlatList
testID="posts"
data={posts}
renderItem={({item: {id, title}, index}) =>
<Text testID="post" key={id}>{title}</Text>
}
/>;
};
describe("Posts", () => {
const getFirstChildText = el =>
typeof el === "string" ? el :
el?.children.find(getFirstChildText)
;
beforeEach(() => {
global.fetch = jest.fn(url => Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve([
{id: 1, title: "foo title"},
{id: 2, title: "bar title"},
])
}));
});
it("should fetch posts", async () => {
const {findAllByTestId} = render(<Posts />);
const posts = await findAllByTestId("post", {timeout: 500});
expect(posts).toHaveLength(2);
expect(getFirstChildText(posts[0])).toEqual("foo title");
expect(getFirstChildText(posts[1])).toEqual("bar title");
expect(fetch).toHaveBeenCalledTimes(1);
});
});
Это не дает мне ничего
act
предупреждения, но у меня была своя доля таких. Эта открытая проблема GitHub, по-видимому, является каноническим ресурсом.
Используемые пакеты:
{
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-native": "^0.64.0",
"react-native-web": "^0.15.6"
},
"devDependencies": {
"@babel/core": "^7.13.15",
"@testing-library/jest-native": "^4.0.1",
"@testing-library/react-native": "^7.2.0",
"babel-jest": "^26.6.3",
"jest": "^26.6.3",
"metro-react-native-babel-preset": "^0.65.2",
"react-test-renderer": "^17.0.2"
}
}
Вы можете использоватьuseEffects
в ваших тестах RNTL довольно легко:
import { render, act } from '@testing-library/react-native';
import { ComponentUnderTest } from './componentundertest.tsx';
test('it updates content on successful call', () => {
render(<ComponentUnderTest />)
expect(await screen.findByText('Results)).toBeTruthy(); // A
})
Нет необходимости использоватьact
напрямую, RNTL использует его для вас под крючком.
Точный предикат для использования в строкеA
зависит от изменений компонентов, которые вы делаете в своемuseEffect
перезвонить. Здесь я просто предполагаю, что при успешном извлечении есть некоторыйText
компонент, отображающий текст «Результаты».
Важно отметить, что ваша выборка, вероятно, асинхронная, поэтому вам нужно использоватьfindBy*
запросы, которые будут ожидать выполнения асинхронного действия (тайм-аут по умолчанию ~5000 мс, его можно настроить).
Еще одно замечание: рекомендуется имитировать сетевые вызовы, чтобы ваши тесты не вызывали настоящий API. Этому есть разные причины: скорость выполнения теста, стабильность теста, не всегда удается достичь желаемого ответа API для целей тестирования и т. д. Рекомендованным инструментом будет библиотека MSW .