使用 Formik 和 Yup 作为验证模式时 React Native 测试失败

Ray*_*han 2 reactjs react-native yup formik react-native-testing-library

我正在尝试为使用 Formik 构建的 React Native 组件编写一些测试。这是一个简单的表单,要求输入用户名和密码,我想使用使用 Yup 构建的验证模式。

当我使用模拟器并手动测试表单时,表单的行为符合预期,仅当输入值无效时才会显示错误消息。

但是,当我尝试使用 编写一些自动化测试时@testing-library/react-native,其行为并不是我所期望的。即使提供的值有效,错误消息也会在测试中显示。下面是代码:

// App.test.js
import React from 'react';
import { render, act, fireEvent } from '@testing-library/react-native';

import App from '../App';

it('does not show error messages when input values are valid', async () => {
  const {
    findByPlaceholderText,
    getByPlaceholderText,
    getByText,
    queryAllByText,
  } = render(<App />);

  const usernameInput = await findByPlaceholderText('Username');
  const passwordInput = getByPlaceholderText('Password');
  const submitButton = getByText('Submit');

  await act(async () => {
    fireEvent.changeText(usernameInput, 'testUser');
    fireEvent.changeText(passwordInput, 'password');
    fireEvent.press(submitButton);
  });

  expect(queryAllByText('This field is required')).toHaveLength(0);
});
Run Code Online (Sandbox Code Playgroud)
// App.js
import React from 'react';
import { TextInput, Button, Text, View } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';

const Schema = Yup.object().shape({
  username: Yup.string().required('This field is required'),
  password: Yup.string().required('This field is required'),
});

export default function App() {
  return (
    <View>
      <Formik
        initialValues={{ username: '', password: '' }}
        validationSchema={Schema}
        onSubmit={(values) => console.log(values)}>
        {({
          handleChange,
          handleBlur,
          handleSubmit,
          values,
          errors,
          touched,
          validateForm,
        }) => {
          return (
            <>
              <View>
                <TextInput
                  onChangeText={handleChange('username')}
                  onBlur={handleBlur('username')}
                  value={values.username}
                  placeholder="Username"
                />
                {errors.username && touched.username && (
                  <Text>{errors.username}</Text>
                )}
              </View>

              <View>
                <TextInput
                  onChangeText={handleChange('password')}
                  onBlur={handleBlur('password')}
                  value={values.password}
                  placeholder="Password"
                />
                {errors.password && touched.password && (
                  <Text>{errors.password}</Text>
                )}
              </View>

              <View>
                <Button
                  onPress={handleSubmit}
                  // If I explicitly call validateForm(), the test will pass
                  // onPress={async () => {
                  //   await validateForm();
                  //   handleSubmit();
                  // }}
                  title="Submit"
                />
              </View>
            </>
          );
        }}
      </Formik>
    </View>
  );
}
Run Code Online (Sandbox Code Playgroud)

我不确定我是否正确地编写了测试。handleSubmit我认为 Formik 会在调用函数时自动验证表单。

在 中App.js,如果我显式调用validateForm,测试就会通过。onPress然而,仅仅为了满足测试而改变处理程序的实现感觉并不正确。也许我缺少一些关于这个问题的基本概念。任何见解都会有帮助,谢谢。


封装版本:

"@testing-library/react-native": "^7.1.0",
"formik": "^2.2.6",
"react": "16.13.1",
"react-native": "0.63.4",
"yup": "^0.32.8"
Run Code Online (Sandbox Code Playgroud)

Ray*_*han 10

终于有时间再回顾一下这个问题了。虽然我还不能 100% 确定幕后发生了什么,但我认为我发现的结果可能会让其他人受益,所以我将在这里分享。

这个问题与两个子问题交织在一起。Promise第一个与 's 模块中使用的相关React Native,第二个与验证的异步性质相关Formik

下面是修改后的代码App.test.js,保持App.js不变,

// App.test.js
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';

import App from '../App';

it('does not show error messages when input values are valid', async () => {
  const { getByPlaceholderText, getByText, queryAllByText } = render(<App />);

  const usernameInput = getByPlaceholderText('Username');
  const passwordInput = getByPlaceholderText('Password');
  const submitButton = getByText('Submit');

  await waitFor(() => {
    fireEvent.changeText(usernameInput, 'testUser');
  });

  await waitFor(() => {
    fireEvent.changeText(passwordInput, 'password');
  });

  fireEvent.press(submitButton);

  await waitFor(() => {
    expect(queryAllByText('This field is required')).toHaveLength(0);
  });
});
Run Code Online (Sandbox Code Playgroud)

通常,我们不需要使用act来包装 the fireEvent,因为默认情况fireEvent下,act由于testing-library. 但是,由于Formik在文本值更改后异步执行验证,并且验证函数不由 的调用React堆栈管理,因此我们需要fireEvent使用act或其他方便的方法手动包装调用:waitFor。简而言之,由于它的异步性,我们需要用fireEvent.changeTexta包裹它。waitFor

然而,将代码改为上述格式并不能解决所有问题。虽然测试通过了,但您会遇到与act. Promise这是一个与 React Native 的 Jest 预设覆盖本机 Promise相关的已知问题。(https://github.com/facebook/react-native/issues/29303

如果你注释掉该行

global.Promise = jest.requireActual('promise');
Run Code Online (Sandbox Code Playgroud)

node_modules/react-native/jest/setup.js第20行左右,这个问题将得到解决。node_modules但不建议直接修改其中的文件。解决方法是设置一个笑话预设来恢复本机 Promise,如下所示(https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938