vitest-testing
Guide for writing unit tests with Vitest for Fastify plugins and routes. Use when creating or modifying tests in app/tests/.
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.
| 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/**(mirrorapp/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
.jsextensions 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