Testing Library 基础查询方法 #

查询方法概述 #

Testing Library 提供了三种类型的查询方法,每种方法有不同的行为和用途:

text
┌─────────────────────────────────────────────────────────────┐
│                     查询方法类型                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  getBy*      → 同步获取,找不到则抛出错误                    │
│  queryBy*    → 同步获取,找不到返回 null                     │
│  findBy*     → 异步获取,返回 Promise                        │
│                                                             │
│  getAllBy*   → 获取所有匹配元素,找不到则抛出错误            │
│  queryAllBy* → 获取所有匹配元素,找不到返回空数组            │
│  findAllBy*  → 异步获取所有匹配元素,返回 Promise            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第一个测试 #

基本组件 #

jsx
function Greeting({ name }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Welcome to our app.</p>
    </div>
  );
}

编写测试 #

jsx
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renders greeting with name', () => {
  render(<Greeting name="World" />);
  
  expect(screen.getByText('Hello, World!')).toBeInTheDocument();
  expect(screen.getByText('Welcome to our app.')).toBeInTheDocument();
});

render 函数 #

基本用法 #

jsx
import { render } from '@testing-library/react';

const { container, unmount, rerender, asFragment } = render(<App />);

render 返回值 #

jsx
test('render returns useful utilities', () => {
  const { 
    container,      // 渲染的 DOM 容器
    baseElement,    // 基础 DOM 元素(通常是 document.body)
    debug,          // 调试函数,打印 DOM
    rerender,       // 重新渲染组件
    unmount,        // 卸载组件
    asFragment,     // 返回 DocumentFragment
    getByText,      // 查询方法(不推荐使用)
  } = render(<App />);
  
  expect(container).toBeInTheDocument();
});

debug 函数 #

jsx
test('debug example', () => {
  const { debug } = render(<Greeting name="World" />);
  
  debug();
  debug(screen.getByText('Hello, World!'));
});

rerender 函数 #

jsx
test('rerender with new props', () => {
  const { rerender } = render(<Greeting name="World" />);
  
  expect(screen.getByText('Hello, World!')).toBeInTheDocument();
  
  rerender(<Greeting name="React" />);
  
  expect(screen.getByText('Hello, React!')).toBeInTheDocument();
});

unmount 函数 #

jsx
test('unmount removes component', () => {
  const { unmount } = render(<Greeting name="World" />);
  
  expect(screen.getByText('Hello, World!')).toBeInTheDocument();
  
  unmount();
  
  expect(screen.queryByText('Hello, World!')).not.toBeInTheDocument();
});

screen 对象 #

为什么使用 screen #

jsx
import { render, screen } from '@testing-library/react';

test('using screen is cleaner', () => {
  render(<App />);
  
  // ✅ 使用 screen - 推荐
  screen.getByText('Hello');
  
  // ❌ 不使用 screen - 不推荐
  const { getByText } = render(<App />);
  getByText('Hello');
});

screen 的优势 #

jsx
test('screen advantages', () => {
  render(<App />);
  
  // 1. 代码更简洁
  screen.getByRole('button');
  
  // 2. 可以使用 debug
  screen.debug();
  
  // 3. 可以使用 logTestingPlaygroundURL
  screen.logTestingPlaygroundURL();
});

查询方法详解 #

getBy* 系列 #

用于断言元素存在:

jsx
test('getBy throws when not found', () => {
  render(<App />);
  
  // ✅ 元素存在时使用
  expect(screen.getByText('Hello')).toBeInTheDocument();
  
  // ❌ 元素不存在时会抛出错误
  // screen.getByText('Not Found'); // Error!
});

queryBy* 系列 #

用于断言元素不存在:

jsx
test('queryBy returns null when not found', () => {
  render(<App />);
  
  // ✅ 断言元素不存在
  expect(screen.queryByText('Error')).not.toBeInTheDocument();
  
  // ✅ 条件判断
  const error = screen.queryByText('Error');
  if (error) {
    // 处理错误
  }
});

findBy* 系列 #

用于异步获取元素:

jsx
test('findBy waits for element', async () => {
  render(<AsyncComponent />);
  
  // ✅ 等待异步元素出现
  const element = await screen.findByText('Loaded');
  expect(element).toBeInTheDocument();
  
  // ✅ 可以设置超时
  await screen.findByText('Slow Load', {}, { timeout: 5000 });
});

getAllBy* 系列 #

获取多个匹配元素:

jsx
function List() {
  return (
    <ul>
      <li>Apple</li>
      <li>Banana</li>
      <li>Orange</li>
    </ul>
  );
}

test('getAllBy returns array', () => {
  render(<List />);
  
  const items = screen.getAllByRole('listitem');
  expect(items).toHaveLength(3);
  expect(items[0]).toHaveTextContent('Apple');
});

queryAllBy* 系列 #

获取多个元素或空数组:

jsx
test('queryAllBy returns empty array when not found', () => {
  render(<App />);
  
  const errors = screen.queryAllByRole('alert');
  expect(errors).toHaveLength(0);
});

findAllBy* 系列 #

异步获取多个元素:

jsx
test('findAllBy waits for elements', async () => {
  render(<AsyncList />);
  
  const items = await screen.findAllByRole('listitem');
  expect(items.length).toBeGreaterThan(0);
});

查询优先级 #

Testing Library 推荐按照以下优先级选择查询方法:

1. getByRole(最推荐) #

jsx
function Button() {
  return <button>Submit</button>;
}

test('use getByRole for accessibility', () => {
  render(<Button />);
  
  // ✅ 最佳实践 - 使用 role
  screen.getByRole('button', { name: /submit/i });
});

常用 ARIA Roles #

jsx
function Form() {
  return (
    <form>
      <h1>Sign Up</h1>
      <label htmlFor="email">Email</label>
      <input id="email" type="email" />
      <button type="submit">Submit</button>
    </form>
  );
}

test('common roles', () => {
  render(<Form />);
  
  screen.getByRole('heading', { name: /sign up/i });
  screen.getByRole('textbox', { name: /email/i });
  screen.getByRole('button', { name: /submit/i });
});

2. getByLabelText #

jsx
function LoginForm() {
  return (
    <form>
      <label htmlFor="username">Username</label>
      <input id="username" />
      
      <label htmlFor="password">Password</label>
      <input id="password" type="password" />
    </form>
  );
}

test('use getByLabelText for form inputs', () => {
  render(<LoginForm />);
  
  screen.getByLabelText('Username');
  screen.getByLabelText('Password');
});

3. getByPlaceholderText #

jsx
function SearchInput() {
  return <input placeholder="Search..." />;
}

test('use getByPlaceholderText for inputs', () => {
  render(<SearchInput />);
  
  screen.getByPlaceholderText('Search...');
});

4. getByText #

jsx
function Nav() {
  return (
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
    </nav>
  );
}

test('use getByText for text content', () => {
  render(<Nav />);
  
  screen.getByText('Home');
  screen.getByText('About');
});

5. getByDisplayValue #

jsx
function ControlledInput() {
  const [value, setValue] = useState('initial');
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

test('use getByDisplayValue for input values', () => {
  render(<ControlledInput />);
  
  screen.getByDisplayValue('initial');
});

6. getByAltText #

jsx
function Avatar() {
  return <img src="/avatar.jpg" alt="User avatar" />;
}

test('use getByAltText for images', () => {
  render(<Avatar />);
  
  screen.getByAltText('User avatar');
});

7. getByTitle #

jsx
function Tooltip() {
  return (
    <span title="More information">
      <svg>...</svg>
    </span>
  );
}

test('use getByTitle for title attribute', () => {
  render(<Tooltip />);
  
  screen.getByTitle('More information');
});

8. getByTestId(最后手段) #

jsx
function DynamicList() {
  return (
    <ul data-testid="dynamic-list">
      <li data-testid="list-item-1">Item 1</li>
      <li data-testid="list-item-2">Item 2</li>
    </ul>
  );
}

test('use getByTestId as last resort', () => {
  render(<DynamicList />);
  
  screen.getByTestId('dynamic-list');
});

查询选项 #

name 选项 #

jsx
function Buttons() {
  return (
    <div>
      <button>Save</button>
      <button>Cancel</button>
    </div>
  );
}

test('name option filters by accessible name', () => {
  render(<Buttons />);
  
  screen.getByRole('button', { name: 'Save' });
  screen.getByRole('button', { name: 'Cancel' });
  
  // 使用正则表达式
  screen.getByRole('button', { name: /save/i });
});

hidden 选项 #

jsx
function HiddenElement() {
  return (
    <div>
      <button style={{ display: 'none' }}>Hidden</button>
      <button>Visible</button>
    </div>
  );
}

test('hidden option includes hidden elements', () => {
  render(<HiddenElement />);
  
  // 默认不包含隐藏元素
  expect(screen.queryByRole('button', { name: 'Hidden' })).not.toBeInTheDocument();
  
  // 使用 hidden: true 包含隐藏元素
  screen.getByRole('button', { name: 'Hidden', hidden: true });
});

exact 选项 #

jsx
function Greeting() {
  return <h1>Hello World</h1>;
}

test('exact option for matching', () => {
  render(<Greeting />);
  
  // 默认精确匹配
  screen.getByText('Hello World');
  
  // 关闭精确匹配
  screen.getByText('Hello', { exact: false });
});

正则表达式匹配 #

基本用法 #

jsx
function UserCard({ name }) {
  return <div>User: {name}</div>;
}

test('regex matching', () => {
  render(<UserCard name="John Doe" />);
  
  // 完全匹配
  screen.getByText('User: John Doe');
  
  // 部分匹配
  screen.getByText(/John Doe/);
  
  // 不区分大小写
  screen.getByText(/john doe/i);
  
  // 开头匹配
  screen.getByText(/^User:/);
  
  // 结尾匹配
  screen.getByText(/Doe$/);
});

复杂匹配 #

jsx
function Price({ amount }) {
  return <span>Price: ${amount.toFixed(2)}</span>;
}

test('complex regex patterns', () => {
  render(<Price amount={99.99} />);
  
  // 匹配价格格式
  screen.getByText(/Price: \$\d+\.\d{2}/);
  
  // 使用 RegExp 构造函数
  const pattern = new RegExp(`Price: \\$${99.99}`);
  screen.getByText(pattern);
});

自定义文本匹配 #

函数匹配器 #

jsx
function ProductList({ products }) {
  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>{p.name} - ${p.price}</li>
      ))}
    </ul>
  );
}

test('function matcher', () => {
  const products = [
    { id: 1, name: 'Apple', price: 1.99 },
    { id: 2, name: 'Banana', price: 0.99 },
  ];
  
  render(<ProductList products={products} />);
  
  // 使用函数匹配
  screen.getByText((content, element) => {
    return content.includes('Apple') && content.includes('1.99');
  });
});

错误处理 #

元素未找到错误 #

jsx
test('element not found error', () => {
  render(<App />);
  
  // 抛出错误并显示可用的元素
  screen.getByRole('button', { name: 'Non-existent' });
  
  // Error: Unable to find an accessible element with the role "button"
  // and name "Non-existent"
  //
  // Here are the accessible roles:
  //   heading:
  //   Name "Welcome":
  //   <h1 />
  //   ...
});

多个元素匹配错误 #

jsx
function MultipleButtons() {
  return (
    <div>
      <button>Click</button>
      <button>Click</button>
    </div>
  );
}

test('multiple elements error', () => {
  render(<MultipleButtons />);
  
  // 抛出错误,显示找到多个元素
  screen.getByText('Click');
  
  // Error: Found multiple elements with the text: Click
  // (If this is intentional, use getAllBy* instead)
});

实用技巧 #

使用 within 限定范围 #

jsx
import { render, screen, within } from '@testing-library/react';

function Sidebar() {
  return (
    <aside>
      <h2>Navigation</h2>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
    </aside>
  );
}

test('within limits search scope', () => {
  render(<Sidebar />);
  
  const nav = screen.getByRole('navigation');
  const homeLink = within(nav).getByRole('link', { name: 'Home' });
  const aboutLink = within(nav).getByRole('link', { name: 'About' });
});

使用 container.querySelector #

jsx
test('container.querySelector for edge cases', () => {
  const { container } = render(<App />);
  
  // 仅在无法使用 Testing Library 查询时使用
  const element = container.querySelector('.custom-class');
});

调试技巧 #

jsx
test('debugging techniques', () => {
  render(<App />);
  
  // 打印整个 DOM
  screen.debug();
  
  // 打印特定元素
  screen.debug(screen.getByRole('button'));
  
  // 生成 playground URL
  screen.logTestingPlaygroundURL();
});

最佳实践总结 #

推荐做法 #

jsx
// ✅ 使用语义化查询
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText(/email/i);

// ✅ 使用 screen 对象
render(<App />);
screen.getByText('Hello');

// ✅ 正确选择查询类型
screen.getByText('Hello');        // 断言存在
screen.queryByText('Error');      // 断言不存在
await screen.findByText('Loaded'); // 异步等待

避免做法 #

jsx
// ❌ 使用 data-testid 作为首选
screen.getByTestId('submit-button');

// ❌ 使用 container.querySelector
container.querySelector('button.submit');

// ❌ 使用错误的查询类型
screen.getByText('Error'); // 应该用 queryByText 断言不存在

// ❌ 不使用 screen
const { getByText } = render(<App />);
getByText('Hello');

下一步 #

现在你已经掌握了基础查询方法,接下来学习 查询方法详解 深入了解每种查询方法!

最后更新:2026-03-28