AgentSkillsCN

vitest-testing

使用 Vitest 为 Fastify 插件和路由编写单元测试的指南。在 app/tests/ 中创建或修改测试时使用。

SKILL.md
--- frontmatter
name: vitest-testing
description: Guide for writing unit tests with Vitest for Fastify plugins and routes. Use when creating or modifying tests in app/tests/.

Vitest Testing Patterns

This skill provides patterns for writing unit tests with Vitest in this repository.

Test Structure

  • Unit tests: app/tests/unit/** (mirror app/src/** structure)
  • Integration tests: app/tests/integration/**
  • Mocks: app/tests/mocks/**
  • Helpers: app/tests/helpers/**

Plugin Test Template

typescript
import Fastify from "fastify";
import { afterEach, describe, expect, it } from "vitest";
import pluginName from "../../../src/plugins/plugin-name.js";

describe("Plugin Name", () => {
  let fastify: ReturnType<typeof Fastify>;

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

  it("should register plugin successfully", async () => {
    fastify = Fastify();
    await fastify.register(pluginName);
    await fastify.ready();

    // Test expectations
    expect(fastify.hasDecorator("decoratorName")).toBe(true);
  });
});

Route Test Template

typescript
import Fastify from "fastify";
import { afterEach, describe, expect, it } from "vitest";
import routeName from "../../../src/routes/route-name.js";

describe("GET /endpoint", () => {
  let fastify: ReturnType<typeof Fastify>;

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

  it("should return expected response", async () => {
    fastify = Fastify();
    await fastify.register(routeName);

    const response = await fastify.inject({
      method: "GET",
      url: "/endpoint",
    });

    expect(response.statusCode).toBe(200);
    expect(response.json()).toEqual({ status: "healthy" });
  });

  it("should return 400 for invalid input", async () => {
    fastify = Fastify();
    await fastify.register(routeName);

    const response = await fastify.inject({
      method: "POST",
      url: "/endpoint",
      payload: { invalid: "data" },
    });

    expect(response.statusCode).toBe(400);
  });
});

Key Patterns

Fastify Instance

  • Use Fastify() to create test instances
  • Always call await fastify.ready() after registering plugins
  • Always call await fastify.close() after tests (use afterEach hook)

Request Simulation

Use fastify.inject() for HTTP request simulation:

typescript
const response = await fastify.inject({
  method: "GET",
  url: "/endpoint",
  headers: { authorization: "Bearer token" },
  query: { param: "value" },
  payload: { data: "value" },
});

Response Assertions

typescript
expect(response.statusCode).toBe(200);
expect(response.headers["content-type"]).toContain("application/json");
expect(response.json()).toEqual({ expected: "data" });

Testing Decorators

typescript
it("should decorate fastify instance", async () => {
  const fastify = Fastify();
  await fastify.register(plugin);
  await fastify.ready();

  expect(fastify.hasDecorator("myDecorator")).toBe(true);
  expect(fastify.myDecorator).toBeDefined();

  await fastify.close();
});

Coverage Requirements

  • Provider: V8
  • Minimum thresholds: 70% (lines, functions, branches, statements)
  • Target: 90%+ overall, 100% for critical business logic
  • Scope: Only tests/unit/** affects coverage metrics

V8 Coverage Ignore

Use /* v8 ignore next -- @preserve */ to exclude untestable code:

typescript
// ignore single statement
/* v8 ignore next -- @preserve */
process.on("SIGTERM", () => gracefulShutdown());

// ignore block
/* v8 ignore start -- @preserve */
const handler = setupHandler();
process.on("SIGINT", handler);
/* v8 ignore stop -- @preserve */

The -- @preserve suffix prevents esbuild from stripping comments.

Import Conventions

  • Use .js extensions for relative imports
  • Use import type { ... } from "pkg" for type-only imports
  • Vitest globals are enabled but explicit imports are preferred for better IntelliSense, type checking, and refactoring support:
    typescript
    import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
    

Commands

bash
cd app
npm run test              # Run all tests
npm run test:watch        # Run tests in watch mode
npm run test:coverage     # Run tests with coverage report

Boundaries

  • No real network, filesystem, or database calls in unit tests
  • Each source file should have a matching test file
  • Do not skip tests without documenting the reason
  • Do not add inline Vitest env comments