帮助
支持我们

使用 Preact Testing Library 进行测试

Preact Testing Library 是一个围绕 preact/test-utils 的轻量级包装器。它提供了一组查询方法,用于访问渲染的 DOM,其方式类似于用户在页面上查找元素的方式。此方法允许你编写不依赖于实现细节的测试。因此,当被测组件经过重构时,这使得测试更容易维护且更具弹性。

Enzyme 不同,Preact Testing Library 必须在 DOM 环境中调用。



安装

通过以下命令安装 testing-library Preact 适配器

npm install --save-dev @testing-library/preact

注意:此库依赖于 DOM 环境的存在。如果你正在使用 Jest,它已经包含在内并默认启用。如果你正在使用其他测试运行器,如 MochaJasmine,你可以通过安装 jsdom 为节点添加一个 DOM 环境。

用法

假设我们有一个 Counter 组件,它显示一个初始值,并带有一个更新它的按钮

import { h } from 'preact';
import { useState } from 'preact/hooks';

export function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  const increment = () => setCount(count + 1);

  return (
    <div>
      Current value: {count}
      <button onClick={increment}>Increment</button>
    </div>
  );
}

我们希望验证我们的计数器显示初始计数,并且单击按钮将对其进行递增。使用你选择的测试运行器,如 JestMocha,我们可以写下这两个场景

import { expect } from 'expect';
import { h } from 'preact';
import { render, fireEvent, screen, waitFor } from '@testing-library/preact';

import Counter from '../src/Counter';

describe('Counter', () => {
  test('should display initial count', () => {
    const { container } = render(<Counter initialCount={5}/>);
    expect(container.textContent).toMatch('Current value: 5');
  });

  test('should increment after "Increment" button is clicked', async () => {
    render(<Counter initialCount={5}/>);

    fireEvent.click(screen.getByText('Increment'));
    await waitFor(() => {
      // .toBeInTheDocument() is an assertion that comes from jest-dom.
      // Otherwise you could use .toBeDefined().
      expect(screen.getByText("Current value: 6")).toBeInTheDocument();
    });
  });
});

你可能已经注意到那里的 waitFor() 调用。我们需要它来确保 Preact 有足够的时间渲染到 DOM 并刷新所有待处理的效果。

test('should increment counter", async () => {
  render(<Counter initialCount={5}/>);

  fireEvent.click(screen.getByText('Increment'));
  // WRONG: Preact likely won't have finished rendering here
  expect(screen.getByText("Current value: 6")).toBeInTheDocument();
});

在底层,waitFor 重复调用传递的回调函数,直到它不再抛出错误或超时(默认:1000 毫秒)。在上面的示例中,我们知道更新已完成,当计数器递增并且新值被渲染到 DOM 中时。

我们还可以使用查询的“findBy”版本而不是“getBy”来以异步优先的方式编写测试。异步查询在底层使用 waitFor 重试,并返回 Promise,因此你需要等待它们。

test('should increment counter", async () => {
  render(<Counter initialCount={5}/>);

  fireEvent.click(screen.getByText('Increment'));

  await screen.findByText('Current value: 6'); // waits for changed element

  expect(screen.getByText("Current value: 6")).toBeInTheDocument(); // passes
});

查找元素

有了完整的 DOM 环境,我们可以直接验证我们的 DOM 节点。通常,测试会检查属性是否存在,比如输入值或元素是否出现/消失。为此,我们需要能够在 DOM 中找到元素。

使用内容

Testing Library 的理念是“你的测试越像你的软件使用方式,它们就能给你越大的信心”。

与页面交互的推荐方式是通过文本内容以用户的方式查找元素。

你可以在 Testing Library 文档的“我应该使用哪个查询” 页面上找到选择正确查询的指南。最简单的查询是 getByText,它查看元素的 textContent。还有用于标签文本、占位符、标题属性等的查询。getByRole 查询是最强大的,因为它抽象了 DOM,并允许你在辅助功能树中查找元素,而辅助功能树是屏幕阅读器读取你的页面方式。组合 roleaccessible name 在单个查询中涵盖了许多常见的 DOM 遍历。

import { render, fireEvent, screen } from '@testing-library/preact';

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // Locate the input using textbox role and the accessible name,
  // which is stable no matter if you use a label element, aria-label, or
  // aria-labelledby relationship
  const field = await screen.findByRole('textbox', { name: 'Sign In' });

  // type in the field
  fireEvent.change(field, { value: 'user123' });
})

有时,当内容发生很大变化时,或者如果你使用将文本翻译成不同语言的国际化框架时,直接使用文本内容会产生摩擦。你可以通过将文本视为你快照的数据来解决此问题,使其易于更新,但将真实来源保留在测试之外。

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // What if we render the app in another language, or change the text? Test fails.
  const field = await screen.findByRole('textbox', { name: 'Sign In' });
  fireEvent.change(field, { value: 'user123' });
})

即使你没有使用翻译框架,你也可以将字符串保存在单独的文件中,并使用与以下示例中相同的策略

test('should be able to sign in', async () => {
  render(<MyLoginForm />);

  // We can use our translation function directly in the test
  const label = translate('signinpage.label', 'en-US');
  // Snapshot the result so we know what's going on
  expect(label).toMatchInlineSnapshot(`Sign In`);

  const field = await screen.findByRole('textbox', { name: label });
  fireEvent.change(field, { value: 'user123' });
})

使用测试 ID

测试 ID 是添加到 DOM 元素的数据属性,以帮助在选择内容模棱两可或不可预测的情况下,或者从实现细节(如 DOM 结构)中分离出来。当其他查找元素的方法都没有意义时,可以使用它们。

function Foo({ onClick }) {
  return (
    <button onClick={onClick} data-testid="foo">
      click here
    </button>
  );
}

// Only works if the text stays the same
fireEvent.click(screen.getByText('click here'));

// Works if we change the text
fireEvent.click(screen.getByTestId('foo'));

调试测试

要调试当前 DOM 状态,可以使用 debug() 函数打印 DOM 的美化版本。

const { debug } = render(<App />);

// Prints out a prettified version of the DOM
debug();

提供自定义上下文提供程序

通常,你会得到一个依赖于共享上下文状态的组件。常见的提供程序通常从路由器、状态到有时是主题和其他针对你的特定应用的全局提供程序。对于每个测试用例重复设置这些提供程序可能会很繁琐,因此我们建议通过包装 @testing-library/preact 中的函数来创建自定义 render 函数。

// helpers.js
import { render as originalRender } from '@testing-library/preact';
import { createMemoryHistory } from 'history';
import { FooContext } from './foo';

const history = createMemoryHistory();

export function render(vnode) {
  return originalRender(
    <FooContext.Provider value="foo">
      <Router history={history}>
        {vnode}
      </Router>
    </FooContext.Provider>
  );
}

// Usage like usual. Look ma, no providers!
render(<MyComponent />)

测试 Preact 钩子

使用 @testing-library/preact,我们还可以测试钩子的实现!想象一下,我们希望为多个组件重新使用计数器功能(我知道我们喜欢计数器!)并将其提取到一个钩子中。现在我们想对其进行测试。

import { useState, useCallback } from 'preact/hooks';

const useCounter = () => {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount(c => c + 1), []);
  return { count, increment };
}

与之前一样,其背后的方法类似:我们希望验证我们是否可以递增计数器。因此,我们需要以某种方式调用我们的钩子。这可以通过 renderHook() 函数来完成,该函数会在内部自动创建一个周围组件。该函数在 result.current 下返回当前钩子返回值,我们可以使用它来进行验证

import { renderHook, act } from '@testing-library/preact';
import useCounter from './useCounter';

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter());

  // Initially the counter should be 0
  expect(result.current.count).toBe(0);

  // Let's update the counter by calling a hook callback
  act(() => {
    result.current.increment();
  });

  // Check that the hook return value reflects the new state.
  expect(result.current.count).toBe(1);
});

有关 @testing-library/preact 的更多信息,请查看 https://github.com/testing-library/preact-testing-library .