解决 react native 运行 jest 单元测试时 "not wrapped in act(...)" 界外更新 state 问题

1,864 阅读2分钟

最近在用 expo 开发 react native 项目,安装配置 jest 后运行 npx jest 之后遇到了这样一个错误:

Expected react-native/jest-preset to define transform[^.+\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$]
react-native/jest-preset contained different transformIgnorePatterns than expected
 PASS  hooks/useTimeline.test.js
 PASS  components/__tests__/StyledText-test.js
  ● Console

    console.error
      Warning: An update to MonoText inside a test was not wrapped in act(...).
      
      When testing, code that causes React state updates should be wrapped into act(...):
      
      act(() => {
        /* fire events that update state */
      });
      /* assert on the output */
      
      This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
          in MonoText

      22 |         console.warn(e);
      23 |       } finally {
    > 24 |         setLoadingComplete(true);
         |         ^
      25 |         SplashScreen.hideAsync();
      26 |       }
      27 |     }

      at warningWithoutStack (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:131:32)
      at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15835:7)
      at loadResourcesAndDataAsync$ (hooks/useCachedResources.ts:24:9)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
      at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:293:22)
      at Generator.next (node_modules/regenerator-runtime/runtime.js:118:21)
      at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)


Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        3.795s
Ran all test suites.

实际上检查 .test.js 文件并没有更新 state 的代码存在:

import * as React from 'react'
import { render } from '@testing-library/react-native'
import { MonoText } from '../StyledText'

it('renders correctly', async () => {
  const result = render(<MonoText>Snapshot test!</MonoText>).toJSON()

  expect(result).toMatchSnapshot()
})

后来看到了这篇文章:Fix the "not wrapped in act(...)" warning

按照文章作者的说法应该是组件中存在着脱离了 jest 控制的 state 更新,也就是修改 state 的语句被放在了 async 函数或者 timer 函数中,jest 都已经测试完毕开始打扫时才收到了这些语句的调用,查看这里 MonoText 的代码:

import * as React from 'react'

import useCachedResources from '@/hooks/useCachedResources'
import { Text, TextProps } from './Themed'

export const MonoText: React.FC<TextProps> = (props) => {
  const isLoadingComplete = useCachedResources()

  if (!isLoadingComplete) {
    return null
  } else {
    return <Text {...props} style={[props.style, { fontFamily: 'space-mono' }]} />
  }
}

再看这里调用的 useCachedResources

import { Ionicons } from '@expo/vector-icons'
import * as Font from 'expo-font'
import * as SplashScreen from 'expo-splash-screen'
import * as React from 'react'

export default function useCachedResources() {
  const [isLoadingComplete, setLoadingComplete] = React.useState(false)

  // Load any resources or data that we need prior to rendering the app
  React.useEffect(() => {
  	/* ↓↓↓ 重点在这里 ↓↓↓ */
    async function loadResourcesAndDataAsync() {
      try {
        SplashScreen.preventAutoHideAsync()
        // Load fonts
        await Font.loadAsync({
          ...Ionicons.font,
          'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
        })
      } catch (e) {
        // We might want to provide this error information to an error reporting service
        console.warn(e)
      } finally {
        setLoadingComplete(true)
        SplashScreen.hideAsync()
      }
    }

    loadResourcesAndDataAsync()
  }, [])

  return isLoadingComplete
}

可以看到应该就是这里的 async 函数 loadResourcesAndDataAsync 导致问题发生,文章作者给出了几种解决方案,这里我选择通过 mock 掉 useCachedResources 来让 jest 感知并控制这个函数:

import * as React from 'react'
import { render } from '@testing-library/react-native'

import { MonoText } from '../StyledText'

/* ↓↓↓ 重点在这里 ↓↓↓ */
jest.mock('@/hooks/useCachedResources')

it('renders correctly', async () => {
  const result = render(<MonoText>Snapshot test!</MonoText>).toJSON()
  expect(result).toMatchSnapshot()
})

重新运行测试:

Expected react-native/jest-preset to define transform[^.+\.(bmp|gif|jpg|jpeg|mp4|png|psd|svg|webp)$]
react-native/jest-preset contained different transformIgnorePatterns than expected
 PASS  hooks/useTimeline.test.js
 PASS  components/__tests__/StyledText-test.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        2.961s, estimated 3s
Ran all test suites.

Well done! 🎉