测试

一些关于测试使用 `nuqs` 的组件的提示

自 nuqs 2 起,您可以单元测试使用 useQueryState(s) 钩子的组件,而无需模拟任何内容,通过使用专用的测试适配器,该适配器将便于设置您的测试(带有初始搜索参数)和在作用于您的组件时断言 URL 更改。

使用 React Testing Library 测试钩子

当使用 React Testing Library 的 renderHook 函数测试依赖 nuqs 的 useQueryState(s) 的钩子时,您可以使用 withNuqsTestingAdapter 获取一个包装组件传递给 renderHook 调用:

import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'

const { result } = renderHook(() => useTheHookToTest(), {
  wrapper: withNuqsTestingAdapter({
    searchParams: { count: "42" },
  }),
})
Introduced in version 2.2.0.

使用 Vitest 测试组件

这里是一个使用 Vitest 和 Testing Library 测试渲染计数器的按钮的示例:

counter-button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { withNuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
  const user = userEvent.setup()
  const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
  render(<CounterButton />, {
    // 1. 通过传递初始搜索参数 / 查询字符串来设置测试:
    wrapper: withNuqsTestingAdapter({ searchParams: '?count=42', onUrlUpdate })
  })
  // 2. 作用
  const button = screen.getByRole('button')
  await user.click(button)
  // 3. 断言状态和(模拟的)URL 中的更改
  expect(button).toHaveTextContent('count is 43')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  const event = onUrlUpdate.mock.calls[0]![0]!
  expect(event.queryString).toBe('?count=43')
  expect(event.searchParams.get('count')).toBe('43')
  expect(event.options.history).toBe('push')
})
counter-button.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { withNuqsTestingAdapter, type OnUrlUpdateFunction } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
  const user = userEvent.setup()
  const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
  render(<CounterButton />, {
    // 1. 通过传递初始搜索参数 / 查询字符串来设置测试:
    wrapper: withNuqsTestingAdapter({ searchParams: '?count=42', onUrlUpdate })
  })
  // 2. 作用
  const button = screen.getByRole('button')
  await user.click(button)
  // 3. 断言状态和(模拟的)URL 中的更改
  expect(button).toHaveTextContent('count is 43')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  const event = onUrlUpdate.mock.calls[0]![0]!
  expect(event.queryString).toBe('?count=43')
  expect(event.searchParams.get('count')).toBe('43')
  expect(event.options.history).toBe('push')
})

有关更多测试相关讨论,请参见 issue #259

Jest 和 ESM

由于 nuqs 2 是一个 仅 ESM 包,要使其与 Jest 配合使用,您需要跳过一些障碍。这是从 Jest ESM 指南 中提取的。

  1. 在您的 jest.config.ts 文件中添加以下选项:
jest.config.ts
const config: Config = {
  // <Other options here>
  extensionsToTreatAsEsm: [".ts", ".tsx"],
  transform: {}
};
  1. 将您的测试命令更改为包含 --experimental-vm-modules 标志:
package.json
{
  "scripts": {
    "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
  }
}

相应地为 Windows 调整,使用 cross-env

API

withNuqsTestingAdapter 接受以下参数:

  • searchParams:用于测试的初始搜索参数。这些可以是查询字符串、URLSearchParams 对象或具有字符串值的记录对象。
withNuqsTestingAdapter({
  searchParams: '?q=hello&limit=10'
})

withNuqsTestingAdapter({
  searchParams: new URLSearchParams('?q=hello&limit=10')
})

withNuqsTestingAdapter({
  searchParams: {
    q: 'hello',
    limit: '10' // 值是序列化的字符串
  }
})
  • onUrlUpdate:当组件更新 URL 时调用的函数。它接收一个对象,其中包含:

    • 新的搜索参数,作为 URLSearchParams 的实例
    • 新的渲染查询字符串(为了方便)
    • 用于更新 URL 的选项。
  • hasMemory:默认情况下,测试适配器是不可变的,意味着它始终使用初始搜索参数作为 URL 更新的基础。这鼓励在单个测试中测试行为单元。

要使其像框架适配器一样行为(这些适配器确实在 URL 中存储更新),设置 hasMemory: true,以便后续更新基于先前状态构建。此内存是每个适配器实例的,因此在测试之间隔离,但在同一适配器下的组件之间共享。

🧪 内部/高级选项
  • rateLimitFactor。默认情况下,测试时禁用限速,因为它可能导致意外行为。将此设置为 1 将启用与生产中相同的限速因子。

  • resetUrlUpdateQueueOnMount:在运行测试前清除 URL 更新队列。这是默认的 true 以隔离测试,但您可以将它设置为 false 以在渲染之间保留 URL 更新队列,并更接近生产行为。

NuqsTestingAdapter

withNuqsTestingAdapter 函数是一个包装组件工厂函数,它使用 NuqsTestingAdapter 包装子组件,但您也可以直接使用它:

import { NuqsTestingAdapter } from 'nuqs/adapters/testing'

<NuqsTestingAdapter>
  <ComponentsUsingNuqs/>
</NuqsTestingAdapter>

它接受与传递给 withNuqsTestingAdapter 的参数相同的 props。

测试自定义解析器

如果您使用 createParser 创建自定义解析器,您可能想要测试它们。

解析器应:

  1. parseserializeeq 定义纯函数。
  2. 是双射的:parse(serialize(x)) === xserialize(parse(x)) === x

为了帮助测试双射性,您可以使用 nuqs/testing 中定义的助手:

import {
  isParserBijective,
  testParseThenSerialize,
  testSerializeThenParse
} from 'nuqs/testing'

it('是双射的', () => {
  // 通过的测试返回 true
  expect(isParserBijective(parseAsInteger, '42', 42)).toBe(true)
  // 失败的测试会抛出错误
  expect(() => isParserBijective(parseAsInteger, '42', 47)).toThrowError()

  // 您也可以单独测试任一侧:
  expect(testParseThenSerialize(parseAsInteger, '42')).toBe(true)
  expect(testSerializeThenParse(parseAsInteger, 42)).toBe(true)
  // 如果测试失败,这些也会抛出错误,从而更容易隔离哪一侧失败:
  expect(() => testParseThenSerialize(parseAsInteger, 'not a number')).toThrowError()
  expect(() => testSerializeThenParse(parseAsInteger, NaN)).toThrowError()
})
Introduced in version 2.4.0.