使用 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,它已经包含在内并默认启用。如果你正在使用其他测试运行器,如 Mocha 或 Jasmine,你可以通过安装 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>
);
}
我们希望验证我们的计数器显示初始计数,并且单击按钮将对其进行递增。使用你选择的测试运行器,如 Jest 或 Mocha,我们可以写下这两个场景
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,并允许你在辅助功能树中查找元素,而辅助功能树是屏幕阅读器读取你的页面方式。组合 role
和 accessible 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 .