最近在项目里用React Testing Library写测试,踩了不少坑,也积累了一些经验,今天就来写一篇实战文章,聊聊React Testing Library的使用方法,以及我踩过的那些坑,希望能给大家一些参考,让大家少走弯路。

一、为什么选择React Testing Library

先说说为什么选择React Testing Library,而不是其他的测试库,比如Enzyme。

以前,我们项目里用的是Enzyme来测试React组件,Enzyme功能很强大,能shallow渲染,能mount渲染,能访问组件的state和props,能模拟事件,用起来很方便。但是,用了一段时间之后,我们发现了一些问题。

首先,Enzyme的测试,很容易依赖组件的实现细节,比如测试的时候,会访问组件的state,会调用组件的方法,会检查组件的props,这样的测试,虽然能通过,但是一旦组件的实现变了,比如改了state的名字,改了方法的名字,测试就会失败,哪怕组件的行为没有变,用户用起来没有任何区别。这样的测试,很脆弱,维护成本很高。

其次,Enzyme的shallow渲染,虽然快,但是不会渲染子组件,很多时候测试不完整,一些集成的问题发现不了。而mount渲染,虽然完整,但是慢,而且容易有副作用,测试之间互相影响,很不稳定。

后来,我们了解到了React Testing Library,它的核心理念是,测试应该关注用户的行为,而不是组件的实现细节。它不提供访问组件state、props、方法的API,而是提供了一系列模拟用户行为的API,比如getByText、getByLabelText、getByRole、fireEvent、waitFor等,让你像用户一样去操作组件,去测试组件的行为。

这样的测试,有几个好处:

  1. 更可靠,因为测试的是用户的行为,而不是实现细节,组件的实现变了,只要行为没变,测试就不会失败,维护成本低。
  2. 更有价值,因为测试的是用户真正会用到的功能,能真正发现用户会遇到的问题,而不是一些实现细节的问题。
  3. 更简单,因为API很少,很容易上手,不需要学习很多复杂的概念,就能写出好的测试。

所以,我们决定把项目里的测试,从Enzyme迁移到React Testing Library,迁移的过程中,踩了不少坑,也积累了一些经验,下面就来分享一下。

二、环境搭建

先说说环境搭建,React Testing Library一般和Jest一起用,Jest是测试运行器,React Testing Library是测试工具库,两者配合使用。

首先,安装依赖:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event jest babel-jest @babel/core @babel/preset-env @babel/preset-react

这里解释一下各个包的作用:

  • @testing-library/react:React Testing Library的核心包,提供渲染、查询、事件等API。
  • @testing-library/jest-dom:提供了一些自定义的Jest匹配器,比如toBeInTheDocument、toHaveTextContent、toBeVisible等,让断言更简单,更易读。
  • @testing-library/user-event:提供了更高级的用户事件模拟,比如type、click、hover等,比fireEvent更接近真实的用户行为。
  • jest:测试运行器,负责运行测试,提供断言、mock等功能。
  • babel-jest@babel/core@babel/preset-env@babel/preset-react:Babel相关的包,用来转译JSX和ES6+代码,让Jest能理解。

然后,配置Babel,在项目根目录创建.babelrc文件:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

如果你的项目用了TypeScript,还要加上@babel/preset-typescript

然后,配置Jest,在项目根目录创建jest.config.js文件:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|webp|svg)$': '<rootDir>/__mocks__/fileMock.js'
  }
};

这里解释一下:

  • testEnvironment: 'jsdom':用jsdom作为测试环境,模拟浏览器的DOM环境,这样才能测试React组件。
  • setupFilesAfterEnv:指定测试运行前的 setup 文件,在这个文件里,可以引入jest-dom,做一些全局的配置。
  • moduleNameMapper:用来mock静态资源,比如CSS、图片等,因为Jest不理解这些文件,需要mock掉。

然后,创建jest.setup.js文件:

import '@testing-library/jest-dom';

这样,就引入了jest-dom的自定义匹配器,可以在所有测试里用了。

如果你的项目用了CSS Module,或者用了一些全局的CSS,还需要做一些额外的配置,这里就不详细说了。

最后,在package.json里加上测试脚本:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

这样,环境就搭建好了,可以开始写测试了。

三、基本用法

环境搭好了,先说说基本用法,写一个简单的测试例子。

假设我们有一个简单的按钮组件,Button.js

import React from 'react';

function Button({ onClick, children }) {
  return (
    <button onClick={onClick} className="btn">
      {children}
    </button>
  );
}

export default Button;

我们来写一个测试,Button.test.js

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('按钮能正常渲染,并且点击时触发onClick', () => {
  // 渲染组件
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>点击我</Button>);

  // 查询元素,用getByText,通过文本内容找到按钮
  const button = screen.getByText('点击我');

  // 断言按钮在文档中
  expect(button).toBeInTheDocument();

  // 模拟点击事件
  fireEvent.click(button);

  // 断言onClick被调用了一次
  expect(handleClick).toHaveBeenCalledTimes(1);
});

这个测试很简单,但是展示了React Testing Library的基本用法:

  1. render渲染组件。
  2. screen.getByText查询元素,通过文本内容找到按钮。
  3. expect做断言,判断元素是否在文档中。
  4. fireEvent.click模拟用户点击。
  5. jest.fn()创建mock函数,判断回调是否被调用。

这就是React Testing Library的基本用法,很简单,很直观,就像用户在操作一样。

四、常用的查询API

React Testing Library提供了很多查询API,用来查找元素,这是最常用的,也是最容易踩坑的地方,下面就来介绍一下常用的查询API。

1. getBy / queryBy / findBy 的区别

这是最容易混淆的,也是最容易踩坑的地方。React Testing Library的查询API,有三种前缀:getBy、queryBy、findBy,它们的区别是:

  • getBy:查询元素,如果找到了,返回元素;如果没找到,或者找到了多个,就会抛出错误,测试失败。适合用在元素一定存在的场景,如果不存在,就是bug,测试应该失败。
  • queryBy:查询元素,如果找到了,返回元素;如果没找到,返回null,不会抛出错误。适合用在元素可能不存在的场景,比如测试元素是否被隐藏,是否不存在。
  • findBy:异步查询元素,返回一个Promise,会等待元素出现,如果在超时时间内找到了,就resolve,返回元素;如果超时了还没找到,就reject,测试失败。适合用在异步渲染的场景,比如组件需要请求数据,数据回来之后才渲染元素。

而且,这三种前缀,都有对应的复数形式:getAllBy、queryAllBy、findAllBy,用来查询多个元素。

举个例子:

// 元素一定存在,用getBy
const button = screen.getByText('提交');

// 元素可能不存在,用queryBy
const errorMessage = screen.queryByText('错误信息');
expect(errorMessage).toBeNull(); // 断言错误信息不存在

// 异步渲染,用findBy
const asyncContent = await screen.findByText('加载完成');

这个区别很重要,很多人踩坑,就是因为用错了前缀,比如异步渲染的元素,用了getBy,结果测试失败,因为元素还没渲染出来;或者元素可能不存在的场景,用了getBy,结果没找到元素,测试失败。

2. 常用的查询方式

除了前缀,还有后缀,也就是查询的方式,常用的有:

  • ByText:通过文本内容查询,适合查询按钮、链接、标题等有文本的元素。
  • ByLabelText:通过label的文本查询,适合查询表单元素,比如input、select等,因为表单元素一般都有label,通过label查询,更符合用户的使用习惯。
  • ByPlaceholderText:通过placeholder查询,适合查询没有label的input,通过placeholder文本查询。
  • ByAltText:通过alt属性查询,适合查询img元素,通过alt文本查询。
  • ByTitle:通过title属性查询,适合查询有title属性的元素。
  • ByRole:通过ARIA role查询,适合查询各种元素,比如button、link、heading、checkbox等,这是最推荐的查询方式,因为它最接近用户的感知,用户就是通过角色来识别元素的。
  • ByTestId:通过data-testid属性查询,这是最后的选择,当其他查询方式都不适用的时候,才用这个,因为它需要在组件里加data-testid属性,有点侵入性。

举个例子:

// 通过文本查询
const submitButton = screen.getByText('提交');

// 通过label查询表单元素
const nameInput = screen.getByLabelText('姓名');

// 通过placeholder查询
const searchInput = screen.getByPlaceholderText('请输入搜索内容');

// 通过alt查询图片
const logo = screen.getByAltText('Logo');

// 通过role查询
const button = screen.getByRole('button', { name: '提交' });
const heading = screen.getByRole('heading', { level: 1 });

// 通过testid查询
const customElement = screen.getByTestId('custom-element');

这里要特别推荐ByRole,因为它最接近用户的感知,而且能同时检查元素的可访问性,比如如果一个按钮没有正确的role,或者没有可访问的名字,就查不到,这样能促使你写出更具可访问性的组件。

而且,ByRole还支持很多选项,比如namelevelcheckeddisabled等,能很精确地查询元素。

五、常见场景的测试方法

介绍完基本用法和查询API,再来说说几个常见场景的测试方法,这些都是我在项目里经常用到的。

1. 测试表单

表单是最常见的组件,也是测试的重点。测试表单,主要是测试输入、验证、提交等功能。

举个例子,一个登录表单:

import React, { useState } from 'react';

function LoginForm({ onSubmit }) {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!username || !password) {
      setError('用户名和密码不能为空');
      return;
    }
    onSubmit({ username, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">用户名</label>
        <input
          id="username"
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="password">密码</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      {error && <div role="alert">{error}</div>}
      <button type="submit">登录</button>
    </form>
  );
}

export default LoginForm;

测试这个表单:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('表单能正常输入和提交', () => {
  const handleSubmit = jest.fn();
  render(<LoginForm onSubmit={handleSubmit} />);

  // 输入用户名和密码
  fireEvent.change(screen.getByLabelText('用户名'), {
    target: { value: 'testuser' }
  });
  fireEvent.change(screen.getByLabelText('密码'), {
    target: { value: 'testpass' }
  });

  // 点击登录按钮
  fireEvent.click(screen.getByText('登录'));

  // 断言onSubmit被调用,并且参数正确
  expect(handleSubmit).toHaveBeenCalledTimes(1);
  expect(handleSubmit).toHaveBeenCalledWith({
    username: 'testuser',
    password: 'testpass'
  });
});

test('表单验证:用户名和密码为空时显示错误', () => {
  render(<LoginForm onSubmit={jest.fn()} />);

  // 直接点击登录,不输入内容
  fireEvent.click(screen.getByText('登录'));

  // 断言错误信息显示
  expect(screen.getByRole('alert')).toHaveTextContent('用户名和密码不能为空');
});

这里要注意,输入表单元素的时候,用fireEvent.change,并且传入{ target: { value: 'xxx' } },模拟用户输入。如果用的是@testing-library/user-event,可以用userEvent.type(input, 'xxx'),更接近真实的用户输入,会触发keydown、keypress、keyup等事件,更可靠。

2. 测试异步组件

异步组件,就是需要请求数据,或者有异步操作的组件,这种组件的测试,需要用findBy或者waitFor来等待异步操作完成。

举个例子,一个用户信息组件,需要请求API获取数据:

import React, { useState, useEffect } from 'react';
import { fetchUser } from './api';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    setLoading(true);
    fetchUser(userId)
      .then((data) => {
        setUser(data);
        setLoading(false);
      })
      .catch((err) => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <div>加载中...</div>;
  if (error) return <div role="alert">{error}</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

export default UserProfile;

测试这个组件,需要mock API请求:

import React from 'react';
import { render, screen } from '@testing-library/react';
import UserProfile from './UserProfile';
import { fetchUser } from './api';

// mock整个api模块
jest.mock('./api');

test('加载成功时显示用户信息', async () => {
  // mock fetchUser的返回值
  fetchUser.mockResolvedValue({
    name: '张三',
    email: 'zhangsan@example.com'
  });

  render(<UserProfile userId={1} />);

  // 先断言加载中显示
  expect(screen.getByText('加载中...')).toBeInTheDocument();

  // 等待用户信息显示,用findBy
  const userName = await screen.findByText('张三');
  expect(userName).toBeInTheDocument();
  expect(screen.getByText('zhangsan@example.com')).toBeInTheDocument();

  // 断言加载中消失了,用queryBy
  expect(screen.queryByText('加载中...')).toBeNull();
});

test('加载失败时显示错误信息', async () => {
  // mock fetchUser抛出错误
  fetchUser.mockRejectedValue(new Error('网络错误'));

  render(<UserProfile userId={1} />);

  // 等待错误信息显示
  const errorMessage = await screen.findByRole('alert');
  expect(errorMessage).toHaveTextContent('网络错误');
});

这里要注意几个点:

  1. jest.mock('./api')mock整个API模块,然后用fetchUser.mockResolvedValue或者fetchUser.mockRejectedValue来mock返回值或者错误。
  2. 异步渲染的元素,用findBy来等待,它会自动轮询,直到元素出现或者超时。
  3. 断言元素不存在的时候,用queryBy,因为getBy找不到会报错。

3. 测试Context和Reducer

如果你的组件用了React Context,或者useReducer,测试的时候,需要把组件包裹在Provider里。

举个例子,一个用了Context的主题切换组件:

// ThemeContext.js
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  return useContext(ThemeContext);
}

// ThemeButton.js
import React from 'react';
import { useTheme } from './ThemeContext';

function ThemeButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      当前主题:{theme === 'light' ? '浅色' : '深色'}
    </button>
  );
}

export default ThemeButton;

测试这个组件,需要包裹在ThemeProvider里:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import ThemeButton from './ThemeButton';

test('主题切换按钮能正常工作', () => {
  render(
    <ThemeProvider>
      <ThemeButton />
    </ThemeProvider>
  );

  // 初始是浅色主题
  const button = screen.getByText('当前主题:浅色');
  expect(button).toBeInTheDocument();

  // 点击切换主题
  fireEvent.click(button);

  // 切换成深色主题
  expect(screen.getByText('当前主题:深色')).toBeInTheDocument();
});

如果很多测试都需要包裹Provider,可以写一个自定义的render函数,把Provider包裹进去:

// test-utils.js
import React from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';

const AllTheProviders = ({ children }) => {
  return <ThemeProvider>{children}</ThemeProvider>;
};

const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

// 重新导出所有的testing-library的API
export * from '@testing-library/react';
// 覆盖原来的render
export { customRender as render };

然后,在测试里,从test-utils导入render,就不用每次都包裹Provider了:

import { render, screen, fireEvent } from './test-utils';
import ThemeButton from './ThemeButton';

test('主题切换按钮能正常工作', () => {
  render(<ThemeButton />);
  // ...
});

这个技巧很实用,尤其是当你的应用有很多Context的时候,能让测试代码更简洁。

六、那些年我踩过的坑

说了这么多用法,再来说说我踩过的那些坑,这些都是我在项目里实际遇到的,希望大家能避免。

坑一:用getBy查询异步渲染的元素,导致测试失败

这是最常见的坑,很多人刚开始用React Testing Library的时候,不知道getBy和findBy的区别,异步渲染的元素,用了getBy,结果测试失败,因为元素还没渲染出来。

比如,上面的UserProfile组件,如果你用screen.getByText('张三'),就会失败,因为数据还没回来,元素还没渲染。这时候,应该用await screen.findByText('张三'),它会等待元素出现。

所以,记住:同步渲染的元素,用getBy;异步渲染的元素,用findBy;元素可能不存在的,用queryBy。

坑二:测试之间互相影响,单独跑通过,一起跑失败

这个坑,很多人都遇到过,测试单独跑的时候都能通过,但是一起跑的时候,有的就失败了,这一般是因为测试之间有共享的状态,互相影响。

比如,你mock了一个模块,在一个测试里改了mock的返回值,没有重置,影响了下一个测试;或者,你在组件里用了全局的变量,测试之间没有清理;或者,你用了setTimeout、setInterval,没有清理,影响了下一个测试。

解决方法:

  1. 每个测试之后,用jest.clearAllMocks()清理mock,或者用beforeEach重置mock。
  2. afterEach(cleanup)清理渲染的组件,React Testing Library会自动清理,但是如果你用了自定义的render,要确保清理了。
  3. 用了定时器的,用jest.useFakeTimers(),并且在测试结束后jest.useRealTimers(),或者用jest.clearAllTimers()清理。
  4. 尽量不要用全局的状态,每个测试都独立,不依赖其他测试的结果。

坑三:fireEvent和真实用户行为不一致,导致测试通过但是实际有问题

fireEvent,是React Testing Library提供的事件模拟工具,但是它只是触发了DOM事件,和真实的用户行为还是有区别的。比如,fireEvent.change(input, { target: { value: 'xxx' } }),只是触发了change事件,设置了value,但是不会触发keydown、keypress、keyup等事件,也不会触发输入框的真实输入行为,如果你的组件里监听了这些事件,或者有一些和输入相关的逻辑,fireEvent可能测试不出来问题。

解决方法:用@testing-library/user-event,它提供了更高级的用户事件模拟,比如userEvent.type(input, 'xxx'),会模拟真实的用户输入,触发keydown、keypress、keyup、input、change等事件,更接近真实的用户行为,也更可靠。

所以,推荐大家用@testing-library/user-event来模拟用户交互,而不是直接用fireEvent,除非是一些user-event不支持的事件。

坑四:用了太多的data-testid,导致测试和实现耦合

data-testid,是React Testing Library提供的最后的查询方式,当其他查询方式都不适用的时候,才用它。但是很多人,为了方便,不管什么元素,都加个data-testid,然后用getByTestId查询,这样的测试,虽然能通过,但是和实现耦合了,因为你需要在组件里加data-testid属性,如果组件改了,data-testid忘了改,测试就会失败。而且,这样的测试,也不关注用户的行为,用户根本看不到data-testid。

解决方法:尽量用用户能感知到的查询方式,比如ByRole、ByText、ByLabelText、ByPlaceholderText等,这些才是用户真正会用到的,也更符合React Testing Library的理念。只有当这些方式都不适用的时候,比如一些自定义的组件,没有文本,没有label,没有role,才用data-testid。

坑五:测试了太多的实现细节,导致测试很脆弱

这个坑,是从Enzyme迁移过来的人最容易犯的,因为Enzyme很容易访问组件的state、props、方法,所以很多人写测试的时候,会测试这些实现细节,比如测试state的值对不对,测试某个方法有没有被调用,测试props有没有传对。这样的测试,虽然能通过,但是很脆弱,组件的实现一变,测试就失败了,哪怕用户的行为没有变。

React Testing Library的理念,就是不要测试实现细节,要测试用户的行为。所以,写测试的时候,要问自己:用户会关心这个吗?用户会操作这个吗?如果答案是否定的,就不要测试。

比如,不要测试组件的state,要测试用户能看到的内容,比如渲染出来的文本、表单的值、错误信息等。不要测试组件的内部方法有没有被调用,要测试用户操作之后,界面有没有变化,回调有没有被调用。不要测试props有没有传对,要测试子组件渲染出来的内容对不对。

这样的测试,才是可靠的,有价值的,维护成本低的。

坑六:异步测试没有等待,导致测试不稳定

异步测试,是最容易不稳定的,有时候通过,有时候失败,这一般是因为没有正确地等待异步操作完成。比如,你用了setTimeout,但是没有等待它执行;你mock了API,但是没有等待数据渲染;你用了Promise,但是没有await。

解决方法:

  1. 异步渲染的元素,用findBy,它会自动等待。
  2. 用了定时器的,用jest.useFakeTimers(),然后用jest.runAllTimers()或者jest.advanceTimersByTime()来推进时间,不要用真实的setTimeout等待。
  3. 有多个异步操作的,用waitFor来等待条件满足,比如await waitFor(() => expect(mockFn).toHaveBeenCalled())
  4. 不要用setTimeout来等待异步操作,比如setTimeout(() => { expect(...) }, 1000),这样的测试很不稳定,应该用findBy或者waitFor。

七、最佳实践

最后,总结一些最佳实践,帮助大家写出更好的测试。

  1. 关注用户行为,不要关注实现细节:这是React Testing Library的核心理念,也是最重要的一点。测试应该像用户一样去操作组件,去测试用户能看到、能用到的功能,而不是测试组件的内部实现。
  1. 优先用ByRole查询:ByRole是最推荐的查询方式,因为它最接近用户的感知,还能检查可访问性。其次是ByText、ByLabelText、ByPlaceholderText等,最后才是ByTestId。
  1. 用user-event模拟用户交互:user-event比fireEvent更接近真实的用户行为,更可靠,推荐优先使用。
  1. 每个测试独立,不依赖其他测试:每个测试都应该能独立运行,不依赖其他测试的结果,也不影响其他测试。测试之间要清理状态,清理mock,清理定时器。
  1. 异步操作用findBy和waitFor:异步渲染的元素,用findBy等待;复杂的异步条件,用waitFor等待。不要用setTimeout等待,也不要用真实的时间等待。
  1. 不要测试太多,也不要测试太少:测试不是越多越好,也不是越少越好,要测试关键的功能,关键的用户路径,保证核心功能正常。不要测试一些无关紧要的细节,也不要漏掉关键的功能。
  1. 测试要易读,易维护:测试代码也是代码,也要写得清晰,易读,易维护。用有意义的测试名称,用清晰的步骤,用合适的注释,让别人能看懂你的测试在测什么。
  1. 持续集成,经常运行测试:写了测试,就要经常运行,保证测试一直通过。可以把测试加到CI/CD流程里,每次提交代码都自动运行测试,有问题及时发现,及时修复。

八、写在最后

React Testing Library,是一个非常优秀的React测试库,它的理念很先进,用法很简单,写出来的测试更可靠,更有价值,维护成本更低。如果你还在用Enzyme,或者还没有写测试,强烈推荐你试试React Testing Library,相信你一定会喜欢上它。

当然,React Testing Library也不是银弹,它也有自己的适用场景,也有一些不足,比如对于一些复杂的动画、拖拽、Canvas等,测试起来比较困难,这时候可能需要其他的工具,或者E2E测试来补充。但是,对于大部分的React组件,React Testing Library都能很好地胜任。

这篇文章,分享了我在使用React Testing Library过程中的一些经验和踩过的坑,希望能给大家一些参考,让大家少走弯路。如果大家有什么问题,或者有什么其他的经验,欢迎在评论区交流。

最后,测试是软件开发中非常重要的一环,它能保证代码的质量,能让我们重构的时候有信心,能让我们交付的时候更放心。希望大家都能重视测试,写出高质量的测试,交付高质量的软件。