测试工具

本页详细介绍 VextJS 的测试工具 API,包括 createTestAppTestAppTestRequestTestRequestBuilderTestResponse

概述

VextJS 提供零配置的测试工具,通过 vextjs/testing 子路径导入:

import { createTestApp } from 'vextjs/testing';

核心设计

  • 零网络 I/OTestRequest 内部不启动 HTTP 服务器,直接通过 adapter.buildHandler() 构造 (req, res) handler,用内存中的 Mock 对象模拟请求。比 supertest 更快,CI 中可并行运行无端口冲突。
  • 零配置:默认禁用限流、静默日志、随机端口,开箱即用。
  • 链式 API:类似 supertest 风格的链式请求构造器,支持 await 直接获取响应。
  • 安全退出config._testMode = true 阻止 shutdown() 调用 process.exit(0)

createTestApp

createTestApp 是测试用 App 工厂函数,创建一个完整的测试应用实例。

函数签名

async function createTestApp(
  options?: CreateTestAppOptions
): Promise<TestApp>;

基本用法

import { describe, it, expect, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';

describe('用户接口', () => {
  let testApp;

  afterEach(async () => {
    await testApp?.close();
  });

  it('GET /users/list 应返回用户列表', async () => {
    testApp = await createTestApp();

    const res = await testApp.request.get('/users/list').query({ page: '1', limit: '10' });

    expect(res.status).toBe(200);
    expect(res.body.code).toBe(0);
    expect(Array.isArray(res.body.data)).toBe(true);
  });
});

返回值

返回 Promise<TestApp>,包含 apprequestclose 三个成员。


CreateTestAppOptions

createTestApp 的配置选项。所有字段均为可选。

interface CreateTestAppOptions {
  config?: Partial<VextConfig>;
  plugins?: boolean;
  setupPlugins?: (app: VextApp) => Promise<void> | void;
  services?: boolean;
  mockServices?: Partial<VextServices>;
  routes?: boolean;
  middlewares?: boolean;
  rootDir?: string;
}

字段说明

字段类型默认值说明
configPartial<VextConfig>{}覆盖默认配置(深度合并到测试默认配置之上)
pluginsbooleanfalse是否加载 src/plugins/(测试环境默认不加载)
setupPluginsFunctionundefined手动注册插件(替代自动扫描)
servicesbooleantrue是否加载 src/services/
mockServicesPartial<VextServices>undefined手动注入 mock services
routesbooleantrue是否加载 src/routes/
middlewaresbooleantrue是否加载 src/middlewares/
rootDirstringprocess.cwd()项目根目录(用于定位 src/ 子目录)

config

覆盖测试默认配置,深度合并。

testApp = await createTestApp({
  config: {
    adapter: 'fastify',       // 测试特定 adapter
    response: { wrap: false }, // 禁用出口包装
    cors: { enabled: false },  // 禁用 CORS
  },
});

测试默认配置(自动应用,无需手动设置):

{
  port: 0,                // 随机端口,避免冲突
  host: '127.0.0.1',
  logger: { level: 'silent' },  // 日志静默
  rateLimit: {
    enabled: false,        // 禁用限流
    max: 100,
    window: 60,
    message: 'Too Many Requests',
    keyBy: 'ip',
  },
  shutdown: { timeout: 1 },  // 快速关闭(1 秒)
  _testMode: true,            // 阻止 process.exit()
}

配置合并优先级:测试默认值DEFAULT_CONFIGconfig 参数


plugins

控制是否自动扫描 src/plugins/ 目录加载插件。

// 默认不加载插件(单元测试通常不需要真实插件)
testApp = await createTestApp(); // plugins: false

// 集成测试可能需要加载插件
testApp = await createTestApp({ plugins: true });

setupPlugins

手动注册插件,替代自动扫描。适用于需要精确控制测试依赖的场景。

testApp = await createTestApp({
  setupPlugins: async (app) => {
    // 只注册测试需要的插件
    app.extend('cache', new MockCache());
    app.extend('db', new MockDatabase());
  },
});
Tip

setupPluginsplugins: true 可以同时使用。此时先执行自动扫描的插件,再执行 setupPlugins。但通常建议只使用其中一种方式。


services

控制是否自动加载 src/services/ 目录的服务。

// 加载真实服务(默认)
testApp = await createTestApp(); // services: true

// 不加载服务(纯路由测试 + mock)
testApp = await createTestApp({
  services: false,
  mockServices: {
    user: {
      findAll: async () => [{ id: '1', name: 'Alice' }],
      findById: async (id) => ({ id, name: 'Test User' }),
    },
  },
});

mockServices

手动注入 mock 服务,覆盖 service-loader 扫描结果。

const mockUserService = {
  findAll: vi.fn().mockResolvedValue([
    { id: '1', name: 'Alice' },
    { id: '2', name: 'Bob' },
  ]),
  findById: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
  create: vi.fn().mockImplementation(async (data) => ({
    id: '3',
    ...data,
  })),
  update: vi.fn().mockResolvedValue({ id: '1', name: 'Updated' }),
  delete: vi.fn().mockResolvedValue(undefined),
};

testApp = await createTestApp({
  mockServices: {
    user: mockUserService,
  },
});

合并逻辑

servicesmockServices行为
true有值先加载真实服务,再用 mockServices 覆盖同名服务
true无值仅使用真实服务
false有值仅使用 mockServices
false无值app.services 为空对象 {}

routes

控制是否自动加载 src/routes/ 目录的路由文件。

// 加载真实路由(默认,集成测试)
testApp = await createTestApp(); // routes: true

// 不加载路由(单元测试服务层时不需要路由)
testApp = await createTestApp({ routes: false });

middlewares

控制是否自动加载 src/middlewares/ 目录的用户中间件。

// 加载用户中间件(默认)
testApp = await createTestApp(); // middlewares: true

// 跳过用户中间件加载(测试不涉及路由级中间件时)
testApp = await createTestApp({ middlewares: false });
Tip

无论 middlewares 设置如何,内置中间件(requestId / cors / bodyParser / responseWrapper / errorHandler)始终会注册。此选项只控制 src/middlewares/ 目录下的用户自定义中间件。


rootDir

项目根目录,用于定位 src/routessrc/servicessrc/plugins 等目录。

import { join } from 'node:path';

testApp = await createTestApp({
  rootDir: join(__dirname, '../../'), // 自定义项目根目录
});

TestApp

createTestApp 返回的测试应用实例。

interface TestApp {
  app: VextApp;
  request: TestRequest;
  close(): Promise<void>;
}

app

底层 VextApp 实例,可用于直接访问应用能力:

const testApp = await createTestApp();

// 访问配置
console.log(testApp.app.config.port);

// 访问服务
const user = await testApp.app.services.user.findById('1');

// 访问日志
testApp.app.logger.info('测试中');

request

HTTP 请求模拟器,类似 supertest 风格的 API。详见 TestRequest

close()

关闭测试应用,触发 onClose 钩子、清理资源。

close(): Promise<void>;
Warning

务必在 afterEachafterAll 中调用 close(),否则会导致资源泄漏(数据库连接、定时器等)和测试进程无法退出。

import { afterEach } from 'vitest';

let testApp;

afterEach(async () => {
  await testApp?.close();
});

TestRequest

HTTP 请求模拟器,提供类似 supertest 风格的链式 API。

interface TestRequest {
  get(path: string): TestRequestBuilder;
  post(path: string): TestRequestBuilder;
  put(path: string): TestRequestBuilder;
  patch(path: string): TestRequestBuilder;
  delete(path: string): TestRequestBuilder;
  options(path: string): TestRequestBuilder;
  head(path: string): TestRequestBuilder;
}

支持的 HTTP 方法

方法说明
request.get(path)发送 GET 请求
request.post(path)发送 POST 请求
request.put(path)发送 PUT 请求
request.patch(path)发送 PATCH 请求
request.delete(path)发送 DELETE 请求
request.options(path)发送 OPTIONS 请求
request.head(path)发送 HEAD 请求

每个方法返回 TestRequestBuilder,支持链式配置后通过 await 执行请求。

基本用法

// GET 请求
const res = await testApp.request.get('/users/list');

// POST 请求
const res = await testApp.request.post('/users').send({
  name: 'Alice',
  email: 'alice@example.com',
});

// PUT 请求
const res = await testApp.request.put('/users/1').send({
  name: 'Alice Updated',
});

// DELETE 请求
const res = await testApp.request.delete('/users/1');

// OPTIONS 请求(CORS 预检)
const res = await testApp.request.options('/users');

TestRequestBuilder

链式请求构造器,支持设置请求头、查询参数、请求体等,最终通过 await.then() 执行请求。

interface TestRequestBuilder extends PromiseLike<TestResponse> {
  set(key: string, value: string): this;
  headers(headers: Record<string, string>): this;
  query(params: Record<string, string | number | boolean>): this;
  send(body: unknown): this;
  type(contentType: string): this;
}
Tip

TestRequestBuilder 实现了 PromiseLike 接口,因此可以直接使用 await 执行请求,无需调用额外的 .execute() 方法。


set(key, value)

设置单个请求头。

set(key: string, value: string): this;
const res = await testApp.request
  .get('/profile')
  .set('Authorization', 'Bearer eyJ...')
  .set('Accept-Language', 'zh-CN');

expect(res.status).toBe(200);

headers(headers)

设置多个请求头(对象形式)。

headers(headers: Record<string, string>): this;
const res = await testApp.request
  .get('/profile')
  .headers({
    'Authorization': 'Bearer eyJ...',
    'Accept-Language': 'zh-CN',
    'X-Custom-Header': 'custom-value',
  });
Tip

set()headers() 可以混合使用,后设置的值会覆盖先设置的同名请求头。


query(params)

设置 URL 查询参数。

query(params: Record<string, string | number | boolean>): this;

参数值会自动转换为字符串并 URL 编码。

const res = await testApp.request
  .get('/users/list')
  .query({ page: 1, limit: 10, active: true });
// 等价于 GET /users/list?page=1&limit=10&active=true

expect(res.status).toBe(200);
expect(res.body.data).toHaveLength(10);

多次调用 query() 会合并参数:

const res = await testApp.request
  .get('/search')
  .query({ keyword: 'hello' })
  .query({ page: 1 });
// 等价于 GET /search?keyword=hello&page=1

send(body)

设置请求体。自动序列化为 JSON 并设置 Content-Type: application/json

send(body: unknown): this;
// JSON 对象
const res = await testApp.request
  .post('/users')
  .send({ name: 'Alice', email: 'alice@example.com' });

expect(res.status).toBe(201);
expect(res.body.data.name).toBe('Alice');
// 嵌套对象
const res = await testApp.request
  .post('/orders')
  .send({
    items: [
      { productId: 'p1', quantity: 2 },
      { productId: 'p2', quantity: 1 },
    ],
    shippingAddress: {
      city: '北京',
      street: '朝阳区xxx路',
    },
  });
// 字符串(直接发送,不做 JSON 序列化)
const res = await testApp.request
  .post('/webhook')
  .type('text/plain')
  .send('raw text body');

type(contentType)

设置 Content-Type 请求头。

type(contentType: string): this;
// 发送 form-urlencoded
const res = await testApp.request
  .post('/login')
  .type('application/x-www-form-urlencoded')
  .send('username=alice&password=secret');

// 发送 XML
const res = await testApp.request
  .post('/xml-endpoint')
  .type('application/xml')
  .send('<user><name>Alice</name></user>');
Tip

send() 会自动设置 Content-Type: application/json(如果尚未设置)。如果需要其他类型,在 send() 之前调用 type() 进行覆盖。


链式组合

所有方法支持链式调用,最终通过 await 执行请求:

const res = await testApp.request
  .post('/users')
  .set('Authorization', 'Bearer eyJ...')
  .set('X-Request-Id', 'test-req-001')
  .query({ notify: 'true' })
  .type('application/json')
  .send({
    name: 'Alice',
    email: 'alice@example.com',
    role: 'admin',
  });

expect(res.status).toBe(201);
expect(res.body.code).toBe(0);
expect(res.body.data.name).toBe('Alice');
expect(res.headers['x-request-id']).toBe('test-req-001');

TestResponse

模拟 HTTP 响应对象,包含状态码、响应头和解析后的响应体。

interface TestResponse {
  status: number;
  headers: Record<string, string>;
  body: any;
  text: string;
}

status

HTTP 状态码。

status: number;
const res = await testApp.request.get('/users/list');
expect(res.status).toBe(200);

const res2 = await testApp.request.get('/users/nonexistent');
expect(res2.status).toBe(404);

const res3 = await testApp.request.post('/users').send({ name: 'Alice', email: 'alice@example.com' });
expect(res3.status).toBe(201);

headers

响应头对象,所有 key 为小写。

headers: Record<string, string>;
const res = await testApp.request.get('/users/list');

// 检查 Content-Type
expect(res.headers['content-type']).toContain('application/json');

// 检查自定义响应头
expect(res.headers['x-request-id']).toBeDefined();

// 检查 CORS 头
expect(res.headers['access-control-allow-origin']).toBeDefined();

body

自动解析的 JSON 响应体。如果响应的 Content-Typeapplication/jsonbody 为解析后的 JavaScript 对象/数组;否则为 undefined

body: any;
const res = await testApp.request.get('/users/list');

// 出口包装格式
expect(res.body).toEqual({
  code: 0,
  data: expect.any(Array),
  requestId: expect.any(String),
});

// 直接访问业务数据
expect(res.body.data).toHaveLength(2);
expect(res.body.data[0].name).toBe('Alice');

错误响应

const res = await testApp.request.get('/users/nonexistent-id');

expect(res.body).toEqual({
  code: -1,
  message: '用户不存在',
  requestId: expect.any(String),
});

带业务错误码

const res = await testApp.request.post('/users').send({
  email: 'existing@example.com',
});

expect(res.body.code).toBe(10001);
expect(res.body.message).toBe('邮箱已注册');

text

原始响应文本。对于 JSON 响应,text 是 JSON 字符串;对于文本响应,text 是原始文本内容。

text: string;
// JSON 响应的原始文本
const res = await testApp.request.get('/users/list');
console.log(res.text);
// '{"code":0,"data":[...],"requestId":"..."}'

// 文本响应
const res2 = await testApp.request.get('/health');
expect(res2.text).toBe('OK');

使用模式

集成测试

加载真实的路由、服务、中间件,验证完整的请求-响应流程:

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';
import type { TestApp } from 'vextjs';

describe('用户 CRUD', () => {
  let testApp: TestApp;

  beforeEach(async () => {
    testApp = await createTestApp({
      plugins: true, // 加载真实插件(如数据库)
    });
  });

  afterEach(async () => {
    await testApp.close();
  });

  it('创建用户', async () => {
    const res = await testApp.request
      .post('/users')
      .set('Authorization', 'Bearer test-admin-token')
      .send({
        name: 'Alice',
        email: 'alice@example.com',
      });

    expect(res.status).toBe(201);
    expect(res.body.code).toBe(0);
    expect(res.body.data).toMatchObject({
      name: 'Alice',
      email: 'alice@example.com',
    });
    expect(res.body.data.id).toBeDefined();
  });

  it('查询用户列表', async () => {
    const res = await testApp.request
      .get('/users/list')
      .query({ page: 1, limit: 10 });

    expect(res.status).toBe(200);
    expect(res.body.code).toBe(0);
    expect(Array.isArray(res.body.data)).toBe(true);
  });

  it('用户不存在应返回 404', async () => {
    const res = await testApp.request.get('/users/nonexistent');

    expect(res.status).toBe(404);
    expect(res.body.message).toBe('用户不存在');
  });

  it('参数校验失败应返回 400', async () => {
    const res = await testApp.request
      .post('/users')
      .set('Authorization', 'Bearer test-admin-token')
      .send({
        name: '', // 不满足 string:1-50
        email: 'invalid-email', // 不满足 email 格式
      });

    expect(res.status).toBe(400);
    expect(res.body.errors).toBeDefined();
    expect(res.body.errors.length).toBeGreaterThan(0);
  });
});

单元测试(Mock Services)

不加载真实服务,使用 mock 替代,专注测试路由逻辑:

import { describe, it, expect, afterEach, vi } from 'vitest';
import { createTestApp } from 'vextjs/testing';

describe('用户路由(mock 服务)', () => {
  let testApp;

  const mockUserService = {
    findAll: vi.fn().mockResolvedValue([
      { id: '1', name: 'Alice' },
      { id: '2', name: 'Bob' },
    ]),
    findById: vi.fn().mockImplementation(async (id) => {
      if (id === '1') return { id: '1', name: 'Alice' };
      return null;
    }),
    create: vi.fn().mockImplementation(async (data) => ({
      id: '3',
      ...data,
      createdAt: new Date().toISOString(),
    })),
  };

  afterEach(async () => {
    await testApp?.close();
    vi.clearAllMocks();
  });

  it('GET /users/list 调用 findAll', async () => {
    testApp = await createTestApp({
      mockServices: { user: mockUserService },
    });

    const res = await testApp.request
      .get('/users/list')
      .query({ page: '1', limit: '10' });

    expect(res.status).toBe(200);
    expect(mockUserService.findAll).toHaveBeenCalledOnce();
  });

  it('GET /users/:id 存在时返回用户', async () => {
    testApp = await createTestApp({
      mockServices: { user: mockUserService },
    });

    const res = await testApp.request.get('/users/1');

    expect(res.status).toBe(200);
    expect(res.body.data.name).toBe('Alice');
    expect(mockUserService.findById).toHaveBeenCalledWith('1');
  });

  it('GET /users/:id 不存在时返回 404', async () => {
    testApp = await createTestApp({
      mockServices: { user: mockUserService },
    });

    const res = await testApp.request.get('/users/999');

    expect(res.status).toBe(404);
  });
});

测试中间件

验证认证、权限等中间件的行为:

import { describe, it, expect, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';

describe('认证中间件', () => {
  let testApp;

  afterEach(async () => {
    await testApp?.close();
  });

  it('无 token 应返回 401', async () => {
    testApp = await createTestApp();

    const res = await testApp.request.post('/users').send({
      name: 'Alice',
      email: 'alice@example.com',
    });

    expect(res.status).toBe(401);
    expect(res.body.message).toContain('认证');
  });

  it('无效 token 应返回 401', async () => {
    testApp = await createTestApp();

    const res = await testApp.request
      .post('/users')
      .set('Authorization', 'Bearer invalid-token')
      .send({
        name: 'Alice',
        email: 'alice@example.com',
      });

    expect(res.status).toBe(401);
  });

  it('有效 token 应正常通过', async () => {
    testApp = await createTestApp();

    const res = await testApp.request
      .post('/users')
      .set('Authorization', 'Bearer valid-admin-token')
      .send({
        name: 'Alice',
        email: 'alice@example.com',
      });

    expect(res.status).not.toBe(401);
  });
});

测试不同 Adapter

验证 Adapter 切换后行为一致:

import { describe, it, expect, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';

const adapters = ['native', 'hono', 'fastify', 'express', 'koa'] as const;

describe.each(adapters)('Adapter: %s', (adapter) => {
  let testApp;

  afterEach(async () => {
    await testApp?.close();
  });

  it('GET /health 应返回 200', async () => {
    testApp = await createTestApp({
      config: { adapter },
    });

    const res = await testApp.request.get('/health');
    expect(res.status).toBe(200);
  });

  it('POST JSON 应正确解析 body', async () => {
    testApp = await createTestApp({
      config: { adapter },
    });

    const res = await testApp.request
      .post('/echo')
      .send({ message: 'hello' });

    expect(res.status).toBe(200);
    expect(res.body.data.message).toBe('hello');
  });
});

测试自定义插件

使用 setupPlugins 注册测试专用插件:

import { describe, it, expect, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';

describe('Redis 缓存插件', () => {
  let testApp;

  afterEach(async () => {
    await testApp?.close();
  });

  it('缓存命中时直接返回', async () => {
    const mockCache = new Map();
    mockCache.set('cache:/users/1', JSON.stringify({ id: '1', name: 'Cached Alice' }));

    testApp = await createTestApp({
      setupPlugins: async (app) => {
        app.extend('cache', {
          get: async (key) => mockCache.get(key) ?? null,
          set: async (key, value, ttl) => mockCache.set(key, value),
          del: async (key) => mockCache.delete(key),
        });
      },
    });

    // 假设路由有缓存中间件,应返回缓存数据
    const res = await testApp.request.get('/users/1');
    expect(res.status).toBe(200);
  });
});

测试错误处理

import { describe, it, expect, afterEach } from 'vitest';
import { createTestApp } from 'vextjs/testing';

describe('错误处理', () => {
  let testApp;

  afterEach(async () => {
    await testApp?.close();
  });

  it('404 路由应返回标准格式', async () => {
    testApp = await createTestApp();

    const res = await testApp.request.get('/nonexistent-path');

    expect(res.status).toBe(404);
    expect(res.body).toMatchObject({
      code: expect.any(Number),
      message: expect.any(String),
      requestId: expect.any(String),
    });
  });

  it('校验失败应返回错误列表', async () => {
    testApp = await createTestApp();

    const res = await testApp.request
      .post('/users')
      .set('Authorization', 'Bearer valid-token')
      .send({}); // 缺少必填字段

    expect(res.status).toBe(400);
    expect(res.body.errors).toBeDefined();
    expect(Array.isArray(res.body.errors)).toBe(true);

    for (const error of res.body.errors) {
      expect(error).toHaveProperty('field');
      expect(error).toHaveProperty('message');
    }
  });

  it('500 错误在生产模式隐藏详情', async () => {
    testApp = await createTestApp({
      config: {
        response: { hideInternalErrors: true },
      },
      mockServices: {
        user: {
          findAll: vi.fn().mockRejectedValue(new Error('数据库连接失败')),
        },
      },
    });

    const res = await testApp.request.get('/users/list');

    expect(res.status).toBe(500);
    expect(res.body.message).toBe('Internal Server Error');
    // 不应暴露内部错误信息
    expect(res.body.message).not.toContain('数据库连接失败');
  });
});

测试出口包装

describe('出口包装', () => {
  it('wrap: true 时响应包含 code/data/requestId', async () => {
    const testApp = await createTestApp({
      config: { response: { wrap: true } },
    });

    const res = await testApp.request.get('/health');

    expect(res.body).toHaveProperty('code', 0);
    expect(res.body).toHaveProperty('data');
    expect(res.body).toHaveProperty('requestId');

    await testApp.close();
  });

  it('wrap: false 时响应为原始数据', async () => {
    const testApp = await createTestApp({
      config: { response: { wrap: false } },
    });

    const res = await testApp.request.get('/health');

    // 原始数据,无 code/data 包装
    expect(res.body).not.toHaveProperty('code');
    expect(res.body).toHaveProperty('status', 'ok');

    await testApp.close();
  });
});

最佳实践

1. 始终调用 close()

afterEach(async () => {
  await testApp?.close();
});

防止资源泄漏导致测试进程挂起。使用 ?. 可选链防止 testApp 未初始化时报错。

2. 每个测试独立创建 TestApp

// ✅ 推荐:每个测试独立 app
it('test 1', async () => {
  testApp = await createTestApp();
  // ...
});

it('test 2', async () => {
  testApp = await createTestApp();
  // ...
});

// ❌ 避免:共享 app(测试间可能互相影响)
// beforeAll(async () => {
//   testApp = await createTestApp();
// });

3. Mock 外部依赖

testApp = await createTestApp({
  services: false,
  mockServices: {
    user: mockUserService,
    email: mockEmailService,
  },
});

单元测试中 mock 掉数据库、外部 API 等依赖,只测试被测代码本身。

4. 使用 vi.fn() 验证调用

const mockService = {
  create: vi.fn().mockResolvedValue({ id: '1' }),
};

testApp = await createTestApp({ mockServices: { user: mockService } });

await testApp.request.post('/users').send({ name: 'Alice' });

expect(mockService.create).toHaveBeenCalledWith(
  expect.objectContaining({ name: 'Alice' })
);

5. 测试描述用中文

describe('用户管理接口', () => {
  it('创建用户成功应返回 201', async () => { ... });
  it('邮箱重复应返回 409', async () => { ... });
  it('未认证应返回 401', async () => { ... });
});

类型导入

// 运行时值
import { createTestApp } from 'vextjs/testing';

// 类型(从主入口导入)
import type {
  CreateTestAppOptions,
  TestApp,
  TestRequest,
  TestRequestBuilder,
  TestResponse,
} from 'vextjs';
Tip

测试工具通过 vextjs/testing 子路径导入(运行时值),类型可从 vextjs 主入口导入。这样设计是为了避免测试依赖(如 mock 相关代码)污染生产代码的打包体积。