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 .

Другие вопросы по тегам