Guides

测试

编辑此页面

测试 Solid 应用程序非常重要,它可以通过防止出现错误导致倒退,进而激发对代码库的信心。


开始

测试包说明

  • vitest - 测试框架,包括运行器、断言引擎和 mock 设施
  • jsdom - 虚拟 DOM,用于模拟在 node 中运行的无头浏览器环境
  • @solidjs/testing-library - 一个用于简化测试组件、指令和原语的库,具有自动清理功能
  • @testing-library/user-event - 用于模拟更接近现实的用户事件
  • @testing-library/jest-dom - 提供了有用的 matchers,加强了断言功能

添加测试包

Solid 应用程序的推荐测试框架是 vitest

要开始使用 vitest,请安装以下开发依赖项:

测试配置

在您的 package.json 中添加一个 test 脚本调用 vitest

package.json
"scripts": {
"test": "vitest"
}

无需将 @testing-library/jest-dom 添加到 vite.config 中的测试选项,因为 vite-plugin-solid 会自动检测并加载它(如果存在)。

TypeScript 配置

如果使用 TypeScript,请将 @testing-library/jest-dom 添加到 tsconfig.json#compilerOptions.types

tsconfig.json
"compilerOptions": {
// ...
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client", "@testing-library/jest-dom"]
}

SolidStart 配置

使用 SolidStart 时,创建一个 vitest.config.ts 文件:

vitest.config.ts
import solid from "vite-plugin-solid"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [solid()],
resolve: {
conditions: ["development", "browser"],
},
})

编写测试

组件测试

组件测试包含三个主要内容:

  • 渲染组件
  • 与组件交互
  • 验证断言

要为组件编写测试,请创建一个 [name].test.tsx 文件。该文件的目的是以单元测试的形式从用户的角度描述预期的行为:

test.jsx 文件中,来自 @solidjs/testing-libraryrender 函数用于渲染组件并提供 props 和上下文。

为了模拟用户交互,使用 @testing-library/user-event

vitest 提供的 expect 函数使用 @testing-library/jest-dom 中的 .ToHaveTextContent("content") 匹配器,提供该组件的预期行为是什么。

要运行此测试,请使用以下命令:

如果运行命令成功,您将得到以下结果,显示测试是否通过或失败:

[RUN] v1.4.0 solid-app/src/components/Counter.test.tsx
src/components/Counter.test.tsx (1)
<Counter /> (1)
increments value
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 16:51:19
Duration 4.34s (transform 1.01s, setup 205ms, collect 1.54s, tests 155ms,
environment 880ms, prepare 212ms)

渲染组件

@solidjs/testing-library 中的 render 函数在 test.tsx 文件中创建测试环境。它设置容器,在其中渲染组件,并自动注册它,测试成功后自动清理。此外,它还将组件包装在上下文中以及设置路由。

const renderResult = render(
() => <MyComponent />, // @solidjs/testing-library requires a function
{ // all options are optional
container, // manually set up your own container, will not be handled
baseElement, // parent of container in case it is not supplied
queries, // manually set up custom queries
hydrate, // set to `true` to use hydration
wrapper, // reusable wrapper component to supply context
location, // sets up a router pointed to the location if provided
}
)
const {
asFragment, // function returning the contents of the container
baseElement, // the parent of the container
container, // the container in which the component is rendered
debug, // a function giving some helpful debugging output
unmount, // manually removing the component from the container
...queries, // functions to select elements from the container
} = renderResult
使用正确的查询

查询是用于查找页面内元素:

⎧ Role
get ⎫ By ⎪ DisplayValue
query ⎬ ⎨ LabelText
find ⎭ AllBy ⎪ Text
⎩ ...

前缀(getqueryfind)和中间部分(ByAllBy )取决于查询是否应该等待元素出现(或不出现)、如果元素没有找到是否应该抛出错误,以及如何处理多个匹配:

  • getBy: 同步,如果未找到或超过 1 个匹配则抛出异常
  • getAllBy: 同步,如果未找到则抛出异常,返回匹配数组
  • queryBy: 同步,如果未找到则为 null,如果超过 1 个匹配则出错
  • queryAllBy: 同步,返回零个或多个匹配项的数组
  • findBy: 异步,如果在 1000 毫秒内未找到或超过 1 个匹配项则 rejected,如果找到则解析元素
  • findAllBy: 异步,如果在 1000 毫秒内未找到则 rejected,解析为包含一个或多个元素的数组

默认情况下,查询应以 get... 开头。如果有多个元素匹配同一查询,则应使用 getAllBy... ,否则使用 getBy...

有两种例外情况不应以 get... 开头:

  1. 如果使用 location 选项或者组件是基于 resources 的,则路由将延迟加载;在这种情况下,渲染后的第一个查询需要是 find...
  2. 当测试未渲染的内容时,您需要找到同时渲染的内容;之后,使用 queryAllBy... 测试结果是否为空数组 ([])。

查询的后缀(RoleLabelText...)取决于您要选择的元素的特征。如果可能,尝试选择可访问的属性(大致按以下顺序):

  • Role: WAI ARIA 由语义元素自动设置,像 <button> 或其他使用 role 属性
  • LabelText: 由标签包裹或 aria-label 属性描述的元素,或者与 for - 或 aria-labelledby 属性链接的元素
  • PlaceholderText: 具有 placeholder 属性的 input 元素
  • Text: 搜索元素中所有文本节点内的文本,即使拆分为多个节点
  • DisplayValue: 显示给定值的表单元素(例如选择元素)
  • AltText: 带有 alt 文本的图像
  • Title: 具有 title 属性的 HTML 元素或具有包含给定文本的 <title> 标记的 SVG
  • TestId: 通过 data-testid 属性查询;可以通过 configure({testIdAttribute: 'data-my-test-attribute'}) 设置不同的数据属性; TestId-queries 不可访问,因此仅将它们用作最后的手段。

有关更多信息,请查看测试库文档

测试 Portal

Solid 允许组件使用 <Portal> 突破 DOM 树结构。该机制在测试中仍然有效,因此 portals 的内容将脱离测试容器。为了测试此内容,请确保使用 screen 导出来查询内容:

在上下文中进行测试

如果组件依赖于某些上下文,请使用 wrapper 选项来包装它:

Context.test.tsx
import { test, expect } from "vitest"
import { render } from "@solidjs/testing-library"
import { DataContext, DataConsumer } from "./Data"
const wrapper = (props) => <DataContext value="test" {...props} />
test("receives data from context", () => {
const { getByText } = render(() => <DataConsumer />, { wrapper })
expect(getByText("test")).toBeInTheDocument()
});

如果包装器是外部创建的,可以重复使用。对于具有不同值的包装器,创建所需包装器的高阶组件可以使测试更加简洁:

const createWrapper = (value) => (props) =>
<DataContext value={value} {...props}/>
测试路由

为了方便起见,render 函数支持 location 选项,该选项将渲染的组件包装在指向给定位置的路由中。由于 <Router> 组件是延迟加载的,因此渲染后的第一个查询需要异步,即 findBy...

const { findByText } = render(
() => <Route path="/article/:id" component={Article} />,
{ location: "/article/12345" }
);
expect(await findByText("Article 12345")).toBeInTheDocument()

与组件交互

许多组件不是静态的,而是根据用户交互而变化。为了测试这些变化,需要模拟这些交互。为了模拟用户交互,可以使用 @testing-library/user-event 库。它负责处理实际用户交互中发生的事件的通常顺序。例如,来自用户的 click 事件将伴随 mousemovehoverkeydownfocuskeyupkeypress

最方便测试的事件通常是 clickkeyboardpointer (模拟触摸事件)。要更深入地了解这些事件,您可以在 user-event 文档中学习。

使用定时器

如果您需要一个假计时器并希望在测试中使用 vi.useFakeTimers(),则必须使用 advanceTimers 选项对其进行设置:

user-event.test.tsx
import { vi } from "vitest"
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
vi.useFakeTimers()
describe("pre-login: sign-in", () => {
const { getByRole, getByLabelText } = render(() => <User />)
const signUp = getByRole('button', { text: 'Sign-in' })
// use convenience API click:
user.click(signUp)
const name = getByLabelText('Name')
// use complex keyboard input:
user.keyboard(name, "{Shift}test{Space}{Shift}user")
const password = getByLabelText('Password')
user.keyboard(name, "secret")
const login = getByRole('button', { text: 'Login' })
// use touch event
user.pointer([
{ keys: "[TouchA]" target: login },
{ keys: "[/TouchA]", target: login }
])
});

验证断言

vitest 附带 expect 函数来增强断言,其工作原理如下:

expect(subject)[assertion](value)

该命令支持开箱即用的断言,例如 toBe(引用比较)和 toEqual(值比较)。为了在 DOM 内部进行测试,@testing-library/jest-dom 通过一些有用的附加断言对其进行了增强:

  • .toBeInTheDocument() - 检查元素是否确实存在于 DOM 中
  • .toBeVisible() - 检查是否隐藏该元素
  • .toHaveTextContent(content) - 检查文本内容是否匹配
  • .toHaveFocus() - 检查这是否是当前聚焦的元素
  • .toHaveAccessibleDescription(description) - 检查可访问的描述
  • 等等.

指令测试

指令是元素的可复用行为。它们接收绑定的 HTML 元素作为第一个参数,接收指令 prop 访问器作为第二个参数。为了使测试更加简洁,@solidjs/testing-library 有一个 renderDirective 函数:

const renderResult = renderDirective(directive, {
initialValue, // value initially added to the argument signal
targetElement, // opt. node name or element used as target for the directive
...renderOptions, // see render options
})
const {
arg, // getter for the directive's argument
setArg, // setter for the directive's argument
...renderResults, // see render results
} = renderResult

...renderResults 中,容器将包含 targetElement ,默认为 <div> 。这与修改 arg 信号的能力一起,在测试指令时非常有用。

例如,如果您有一个处理 Fullscreen API 的指令,您可以像这样测试它:

原语测试

当不需要对元素的引用时,可以将部分状态和逻辑放入可复用的 hooks 或原语中。由于这些不需要元素,因此不需要 render 来测试它们,因为它需要一个没有其他用途的组件。为了避免这种情况,有一个 renderHook 工具函数可以模拟组件而不实际渲染任何内容。

const renderResult = renderHook(hook, {
initialProps, // an array with arguments being supplied to the hook
wrapper, // same as the wrapper optionss for `render`
})
const {
result, // return value of the hook (mutable, destructuring fixes it)
cleanup, // manually remove the traces of the test from the DOM
owner, // the owner running the hook to use with `runWithOwner()`
} = renderResult

一个管理计数器状态的原语可以这样测试:

import { test, expect } from "vitest"
import { renderHook } from "@solidjs/testing-library"
import { createCounter } from "./counter"
test("increments count", () => {
const { result } = renderHook(createCounter)
expect(result.count).toBe(0)
result.increment()
expect(result.count).toBe(1)
})

测试 effects

由于 effects 可能异步发生,因此测试它们可能很困难。 @solidjs/testing-library 提供了一个 testEffect 函数,该函数接受另一个函数,该函数接收 done 函数,在测试结束后调用该函数并返回一个 promise。一旦 done 被调用,返回的 promise 就被 resolve。任何会触及下一个边界的错误都会被用来 reject 返回的 promise。

使用 testEffect 的示例测试可能如下所示:

const [value, setValue] = createSignal(0)
return testEffect(done =>
createEffect((run: number = 0) => {
if (run === 0) {
expect(value()).toBe(0)
setValue(1)
} else if (run === 1) {
expect(value()).toBe(1)
done()
}
return run + 1
})
)

基准测试

虽然 Solid 提供了优化的性能,但最好验证一下是否可以兑现这一承诺。 Vitest 提供了一个实验性的 bench 函数来运行基准测试并比较同一 describe 块内的结果;

例如,如果您有一个类似于 <For><List> 流组件,您可以像这样对它进行基准测试:

list.bench.jsx
describe('list rendering', () => {
const ITEMS = 1000
const renderedFor = new Set()
const listFor = Array.from({ length: ITEMS }, (_, i) => i)
bench('For', () => new Promise((resolve) => {
const ItemFor = (props) => {
onMount(() => {
renderedFor.add(props.number)
if (renderedFor.size === ITEMS) { resolve() }
})
return <span>{props.number}</span>
}
render(() => <For each={listFor}>
{(item) => <ItemFor number={item} />}
</For>)
}))
const renderedList = new Set()
const listList = Array.from({ length: ITEMS }, (_, i) => i)
bench('List', () => new Promise((resolve) => {
const ItemList = (props) => {
onMount(() => {
renderedList.add(props.number)
if (renderedList.size === ITEMS) { resolve() }
})
return <span>{props.number}</span>
}
render(() => <List each={listList}>
{(item) => <ItemList number={item} />}
</List>)
}))
})

运行 [npm|pnpm|yarn] test bench 将执行基准测试函数:

[RUN] v1.4.0 solid-app/src/components/
src/components/list.bench.jsx (2) 1364ms
benchmark (2) 1360ms
name hz min max mean p75 p99 p995 p999 rme samples
· For 60.5492 11.2355 47.9164 16.5155 15.4180 47.9164 47.9164 47.9164 ±13.60% 31 fastest
· List 49.7725 16.5441 69.3559 20.0914 18.0349 69.3559 69.3559 69.3559 ±21.37% 25
[BENCH] Summary
For - src/components/list.bench.tsx > benchmark
1.22x faster than List

请记住,创建有意义的基准非常困难。对于这些数字应该持保留态度,但如果在版本之间进行比较,仍然可以表明性能下降。

测试覆盖率

虽然覆盖率数字可能会产生误导,但许多项目都使用它们作为代码质量的粗略衡量标准。

Vitest支持覆盖率收集。要使用它,需要额外的包:

另外,还需要设置 vitest 的覆盖率功能

集成/端到端测试

有些问题只有在代码在其应该运行的环境中运行后才能发现。由于集成和端到端测试与框架无关,因此所有经过验证的方法都适用于 Solid。

报告此页面问题