vitest-testing

janisto's avatarfrom janisto

Guide for writing unit tests with Vitest for Fastify plugins and routes. Use when creating or modifying tests in app/tests/.

⭐0stars🔀0forks📁View on GitHub🕐Updated Dec 28, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive framework and standardized patterns for writing unit and integration tests using Vitest for Fastify applications. It streamlines the creation of robust test suites for plugins and routes, ensuring high code quality through request simulation, lifecycle management, and strict V8 coverage enforcement.

Use Cases

  • Standardizing unit test structures for Fastify plugins to verify decorators and internal logic registration.
  • Simulating complex HTTP requests using fastify.inject() to validate route handlers, status codes, and JSON payloads.
  • Implementing automated cleanup and resource management in test suites using Vitest lifecycle hooks like afterEach.
  • Configuring and monitoring V8 code coverage to ensure critical business logic meets the 90%+ target threshold.
  • Excluding untestable code blocks from coverage reports using specific V8 ignore patterns while maintaining report integrity.
namevitest-testing
descriptionGuide 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

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

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:

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

Response Assertions

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

Testing Decorators

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:

// 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:
    import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
    

Commands

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