Test tools

This page details VextJS's testing tool API, including createTestApp, TestApp, TestRequest, TestRequestBuilder and TestResponse.

Overview

VextJS provides zero-configuration testing tools, imported through the vextjs/testing subpath:

import { createTestApp } from "vextjs/testing";

Core Design:

  • Zero network I/O: TestRequest does not start the HTTP server internally, directly constructs the (req, res) handler through adapter.buildHandler(), and uses the Mock object in memory to simulate the request. Faster than supertest, can be run in parallel in CI without port conflicts.
  • Zero Configuration: Current limiting, silent logging, and random ports are disabled by default and can be used out of the box.
  • Chained API: A chained request constructor similar to supertest style, supporting await to directly obtain the response.
  • Safe Exit: config._testMode = true prevents shutdown() from calling process.exit(0).

createTestApp

createTestApp is a test App factory function that creates a complete test application instance.

Function signature

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

Basic usage

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

describe("User Interface", () => {
  let testApp;

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

  it("GET /users/list should return a list of users", 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);
  });
});

Return value

Returns Promise<TestApp>, containing three members: app, request and close.


CreateTestAppOptions

Configuration options for createTestApp. All fields are optional.

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

Field description

FieldTypeDefault ValueDescription
configPartial<VextConfig>{}Override the default configuration (deeply merged into the test default configuration)
pluginsbooleanfalseWhether to load src/plugins/ (not loaded by default in the test environment)
setupPluginsFunctionundefinedManually register plugins (replacing automatic scanning)
servicesbooleantrueWhether to load src/services/
mockServicesPartial<VextServices>undefinedManually inject mock services
routesbooleantrueWhether to load src/routes/
middlewaresbooleantrueWhether to load src/middlewares/
rootDirstringprocess.cwd()Project root directory (used to locate the src/ subdirectory)

config

Cover the default configuration of the test and merge deeply.

testApp = await createTestApp({
  config: {
    adapter: "fastify", // test specific adapter
    response: { wrap: false }, // Disable export wrapping
    cors: { enabled: false }, // Disable CORS
  },
});

Testing the default configuration (automatically applied, no manual settings required):

{
  port: 0, // Random port to avoid conflicts
  host: '127.0.0.1',
  logger: { level: 'silent' }, // Log silently
  rateLimit: {
    enabled: false, // disable current limiting
    max: 100,
    window: 60,
    message: 'Too Many Requests',
    keyBy: 'ip',
  },
  shutdown: { timeout: 1 }, // Quick shutdown (1 second)
  _testMode: true, // prevent process.exit()
}

Configure merge priority: test defaultsDEFAULT_CONFIGconfig parameters


plugins

Controls whether to automatically scan the src/plugins/ directory to load plugins.

//Plug-ins are not loaded by default (unit tests usually do not require real plug-ins)
testApp = await createTestApp(); // plugins: false// Integration testing may require loading plugins
testApp = await createTestApp({ plugins: true });

setupPlugins

Manually register plug-ins instead of automatic scanning. Suitable for scenarios where precise control of test dependencies is required.

testApp = await createTestApp({
  setupPlugins: async (app) => {
    //Only register the plug-ins required for testing
    app.extend("testCache", new MockCache());
    app.extend("db", new MockDatabase());
  },
});
Tip

setupPlugins is used to replace automatic scanning: when setupPlugins is passed in, the test tool only executes this function and no longer reads the file system scan triggered by plugins: true. If you need real plug-ins, please use plugins: true; if you need precise control of test dependencies, please only use setupPlugins.


services

Controls whether to automatically load services in the src/services/ directory.

//Load the real service (default, recommended for integration testing)
testApp = await createTestApp(); // services: true

// Do not load the service (unit testing recommendation: use mockServices instead)
testApp = await createTestApp({
  services: false,
  mockServices: {
    user: {
      findAll: vi.fn().mockResolvedValue({ items: [], total: 0 }),
      findById: vi.fn().mockResolvedValue({ id: "1", name: "Test User" }),
    },
  },
});

TypeScript service file loading mechanism

When services: true, service-loader scans the src/services/ directory and automatically loads .ts source files. Due to two limitations of Node.js native ESM, the framework uses esbuild to automatically handle them:

LimitationsDescription
ERR_UNKNOWN_FILE_EXTENSION: .tsNode.js native ESM does not support direct import() .ts files
.js → .ts remapping missingTypeScript ESM convention is to write .js extension in import, and Node.js/Vite resolver will not automatically fall back to .ts

service-loader performs the following process for each .ts service file:

  1. Call esbuild.build({ bundle: true, packages: 'external' }) to package .ts and its local relative dependencies into .mjs
  2. Write the compiled product to a temporary file in the same directory as the source file (the name contains .__vext_compiled__ and will not be scanned repeatedly)
  3. import() temporary .mjs file, automatically cleaned after completion
Recommendation for unit testing: use mockServices first

The services: false + mockServices solution does not require esbuild compilation, is faster and has better isolation, and is the recommended method for unit testing. services: true is suitable for integration tests that require real service behavior.


mockServices

Manually inject the mock service and overwrite the service-loader scan results.

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,
  },
});

Merge Logic:

servicesmockServicesbehavior
trueValidLoad the real service first, then use mockServices to overwrite the service with the same name
trueNo valueOnly use real services
falsehas valueonly use mockServices
falseNo valueapp.services is an empty object {}

routes

Controls whether to automatically load routing files in the src/routes/ directory.

//Load the real route (default, integration test)
testApp = await createTestApp(); // routes: true

// Do not load routes (routing is not required when unit testing the service layer)
testApp = await createTestApp({ routes: false });

middlewares

Controls whether to automatically load user middleware in the src/middlewares/ directory.

//Load user middleware (default)
testApp = await createTestApp(); // middlewares: true

// Skip user middleware loading (when testing does not involve routing-level middleware)
testApp = await createTestApp({ middlewares: false });
Tip

The built-in middlewares (requestId/cors/bodyParser/responseWrapper/errorHandler) are always registered regardless of the middlewares setting. This option only controls user-defined middleware in the src/middlewares/ directory.


rootDir

The project root directory is used to locate src/routes, src/services, src/plugins and other directories.

import { join } from "node:path";

testApp = await createTestApp({
  rootDir: join(__dirname, "../../"), // Customize the project root directory
});

TestApp

The test application instance returned by createTestApp.

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

app

The underlying VextApp instance, which can be used to directly access application capabilities:

const testApp = await createTestApp();

//Access configuration
console.log(testApp.app.config.port);

//Access service
const user = await testApp.app.services.user.findById("1");

//Access log
testApp.app.logger.info("under testing");

request

HTTP request simulator, similar to supertest style API. See TestRequest for details.

close()

Close the test application, trigger the onClose hook, and clean up resources.

close(): Promise<void>;
Warning

Be sure to call close() in afterEach or afterAll, otherwise it will cause resource leaks (database connections, timers, etc.) and the test process cannot exit.

import { afterEach } from "vitest";

let testApp;

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

TestRequest

HTTP request simulator, providing a chained API similar to supertest style.

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;
}

Supported HTTP methods

MethodDescription
request.get(path)Send GET request
request.post(path)Send POST request
request.put(path)Send PUT request
request.patch(path)Send PATCH request
request.delete(path)Send DELETE request
request.options(path)Send OPTIONS request
request.head(path)Send HEAD request

Each method returns TestRequestBuilder, which supports chain configuration and execution of requests through await.

Basic usage

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

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

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

// DELETE request
const res = await testApp.request.delete("/users/1");

// OPTIONS request (CORS preflight)
const res = await testApp.request.options("/users");

TestRequestBuilder

The chained request constructor supports setting request headers, query parameters, request bodies, etc., and finally executes the request through await or .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 implements the PromiseLike interface, so you can directly use await to execute the request without calling an additional .execute() method.


set(key, value)

Set a single request header.

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)

Set multiple request headers (in object form).

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() and headers() can be used together, and the value set later will overwrite the request header with the same name set first.


query(params)

Set URL query parameters.

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

Parameter values are automatically converted to strings and URL encoded.

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

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

Calling query() multiple times merges parameters:

const res = await testApp.request
  .get("/search")
  .query({ keyword: "hello" })
  .query({ page: 1 });
// Equivalent to GET /search?keyword=hello&page=1

send(body)

Set the request body. Automatically serialize to JSON and set Content-Type: application/json.

send(body: unknown): this;
// JSON object
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");
// Nested objects
const res = await testApp.request.post("/orders").send({
  items: [
    { productId: "p1", quantity: 2 },
    { productId: "p2", quantity: 1 },
  ],
  shippingAddress: {
    city: "Beijing",
    street: "xxx Road, Chaoyang District",
  },
});
// String (send directly without JSON serialization)
const res = await testApp.request
  .post("/webhook")
  .type("text/plain")
  .send("raw text body");

type(contentType)

Set the Content-Type request header.

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

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

send() automatically sets Content-Type: application/json if it is not already set. If another type is required, call type() before send() to override it.


Chain combination

All methods support chained calls, and the request is ultimately executed through 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

Simulates an HTTP response object, including status code, response headers and parsed response body.

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

status

HTTP status code.

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

Response header object, all keys are lowercase.

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

// Check Content-Type
expect(res.headers["content-type"]).toContain("application/json");

// Check custom response headers
expect(res.headers["x-request-id"]).toBeDefined();

// Check CORS headers
expect(res.headers["access-control-allow-origin"]).toBeDefined();

body

Automatically parsed JSON response body. If the response's Content-Type is application/json, body is the parsed JavaScript object/array; otherwise it is undefined.

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

//Export packaging format
expect(res.body).toEqual({
  code: 0,
  data: expect.any(Array),
  requestId: expect.any(String),
});// Directly access business data
expect(res.body.data).toHaveLength(2);
expect(res.body.data[0].name).toBe("Alice");

Error response:

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

expect(res.body).toEqual({
  code: -1,
  message: "User does not exist",
  requestId: expect.any(String),
});

With business error code:

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

expect(res.body.code).toBe(10001);
expect(res.body.message).toBe("Email has been registered");

text

Raw response text. For a JSON response, text is the JSON string; for a text response, text is the raw text content.

text: string;
// Raw text of JSON response
const res = await testApp.request.get("/users/list");
console.log(res.text);
// '{"code":0,"data":[...],"requestId":"..."}'

// text response
const res2 = await testApp.request.get("/health");
expect(res2.text).toBe("OK");

Usage mode

Integration testing

Load the real routes, services, and middleware, and verify the complete request-response process:

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

describe("User CRUD", () => {
  let testApp: TestApp;

  beforeEach(async () => {
    testApp = await createTestApp({
      plugins: true, // Load real plugins (such as database)
    });
  });

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

  it("Create user", 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("Query user list", 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 should be returned if the user does not exist", async () => {
    const res = await testApp.request.get("/users/nonexistent");

    expect(res.status).toBe(404);
    expect(res.body.message).toBe("The user does not exist");
  });

  it("Failed parameter verification should return 422", async () => {
    const res = await testApp.request
      .post("/users")
      .set("Authorization", "Bearer test-admin-token")
      .send({
        name: "", // does not satisfy string:1-50
        email: "invalid-email", // does not meet the email format
      });

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

Unit Test (Mock Services)

Do not load real services, use mock instead, focus on testing routing logic:

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

describe("User routing (mock service)", () => {
  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 call 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 returns the user if it exists", 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 returns 404 when it does not exist", async () => {
    testApp = await createTestApp({
      mockServices: { user: mockUserService },
    });

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

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

Test middleware

Verify the behavior of middleware such as authentication and permissions:

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

describe("Authentication Middleware", () => {
  let testApp;

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

  it("No token should return 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("authentication");
  });

  it("Invalid token should return 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("valid token should pass normally", 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);
  });
});

Test different Adapters

Verify that the behavior is consistent after the Adapter is switched:

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 should return 200", async () => {
    testApp = await createTestApp({
      config: { adapter },
    });

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

  it("POST JSON should parse body correctly", 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");
  });
});

Test custom plug-ins

Use setupPlugins to register test-specific plugins:

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

describe("Redis cache plug-in", () => {
  let testApp;

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

  it("Return directly when cache hits", async () => {
    const mockCache = new Map();
    mockCache.set("user:1", JSON.stringify({ id: "1", name: "Cached Alice" }));

    testApp = await createTestApp({
      setupPlugins: async (app) => {
        app.extend("userCache", {
          get: async (key) => mockCache.get(key) ?? null,
          set: async (key, value, ttl) => mockCache.set(key, value),
          del: async (key) => mockCache.delete(key),
        });
      },
    });// Assuming that the route handler will read app.userCache, cached data should be returned
    const res = await testApp.request.get("/users/1");
    expect(res.status).toBe(200);
  });
});

Test error handling

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

describe("Error handling", () => {
  let testApp;

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

  it("404 routing should return standard format", 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("Validation failure should return an error list", async () => {
    testApp = await createTestApp();

    const res = await testApp.request
      .post("/users")
      .set("Authorization", "Bearer valid-token")
      .send({}); // Required fields are missing

    expect(res.status).toBe(422);
    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 error hide details in production mode", async () => {
    testApp = await createTestApp({
      config: {
        response: { hideInternalErrors: true },
      },
      mockServices: {
        user: {
          findAll: vi.fn().mockRejectedValue(new Error("Database connection failed")),
        },
      },
    });

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

    expect(res.status).toBe(500);
    expect(res.body.message).toBe("Internal Server Error");
    // Internal error messages should not be exposed
    expect(res.body.message).not.toContain("Database connection failed");
  });
});

Test export packaging

describe("Export Packaging", () => {
  it("wrap: true when the response contains 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 when the response is original data", async () => {
    const testApp = await createTestApp({
      config: { response: { wrap: false } },
    });

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

    // Raw data, no code/data packaging
    expect(res.body).not.toHaveProperty("code");
    expect(res.body).toHaveProperty("status", "ok");

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

Best Practices

1. Always call close()

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

Prevent resource leaks from causing the test process to hang. Use the ?. optional chain to prevent testApp from reporting an error when it is not initialized.

2. Create TestApp independently for each test

// ✅ Recommendation: each test independent app
it("test 1", async () => {
  testApp = await createTestApp();
  // ...
});

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

// ❌ Avoid: Sharing apps (tests may affect each other)
// beforeAll(async () => {
// testApp = await createTestApp();
// });

3. Mock external dependencies

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

In unit testing, dependencies such as databases and external APIs are mocked, and only the code under test itself is tested.

4. Use vi.fn() to verify the call

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. Test description is in Chinese

describe('User Management Interface', () => {
  it('Successful user creation should return 201', async () => { ... });
  it('Duplicate mailbox should return 409', async () => { ... });
  it('Unauthenticated should return 401', async () => { ... });
});

Type import

// runtime value
import { createTestApp } from "vextjs/testing";

// Type (imported from main entrance)
import type {
  CreateTestAppOptions,
  TestApp,
  TestRequest,
  TestRequestBuilder,
  TestResponse,
} from "vextjs";
Tip

Test tools are imported through the vextjs/testing subpath (runtime value), and types can be imported from the vextjs main entry. This is designed to prevent test dependencies (such as mock-related code) from polluting the packaging volume of the production code.