Skip to main content
This covers effective methods for debugging tests written with Vitest.

Vitest Debugging Methods

Sonamu uses Vitest as its test framework. It provides various debugging methods.

Using VSCode Debugger

1. launch.json Configuration

.vscode/launch.json:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current Test File",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": [
        "vitest",
        "run",
        "${file}"
      ],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true,
      "resolveSourceMapLocations": [
        "${workspaceFolder}/**",
        "!**/node_modules/**"
      ]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug All Tests",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": [
        "vitest",
        "run"
      ],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true
    }
  ]
}

2. Running the Debugger

  1. Open test file
    • Open the .test.ts file you want to debug
  2. Set breakpoints
    • Click to the left of the line number to show a red dot
  3. Start debugger
    • Press F5 or select “Debug Current Test File” from the “Run and Debug” panel
  4. Debug
    • When paused at a breakpoint, inspect variables
    • Use F10 (Step Over), F11 (Step Into), etc.

Console Logging

Using console.log

The simplest debugging method:
test("create user", async () => {
  const email = "test@example.com";

  console.log("Test started:", email);

  const user = await UserModel.create({ email });
  console.log("Created user:", user);

  expect(user.email).toBe(email);
});

Structured Logging

test("logging complex objects", async () => {
  const data = {
    user: { id: 1, email: "test@example.com" },
    items: [1, 2, 3]
  };

  // Pretty print
  console.log(JSON.stringify(data, null, 2));

  // Or
  console.dir(data, { depth: null });
});

Running Specific Tests

test.only

// Run only this test
test.only("test to debug", async () => {
  const result = await complexOperation();
  expect(result).toBeDefined();
});

// Other tests are skipped
test("test to skip", async () => {
  // ...
});

describe.only

// Run only tests in this block
describe.only("User API", () => {
  test("create user", async () => {
    // ...
  });

  test("get user", async () => {
    // ...
  });
});

// Other describe blocks are skipped
describe("Order API", () => {
  test("create order", async () => {
    // ...
  });
});

Running Specific Tests via CLI

# Specific file only
pnpm vitest run src/models/user.model.test.ts

# Specific pattern
pnpm vitest run user

# Specific test name
pnpm vitest run -t "create user"

Watch Mode

Auto Re-run

# Run in watch mode
pnpm vitest watch

# Or
pnpm test --watch
When you modify a file, related tests are automatically re-run.

Watch Mode Shortcuts

During test execution:
  • a: Re-run all tests
  • f: Re-run failed tests only
  • t: Filter specific tests
  • q: Quit

UI Mode (vitest —ui)

Running

pnpm vitest --ui
Opens http://localhost:51204/__vitest__/ in the browser Features:
  • Visualize test hierarchy
  • Filter failed tests
  • Re-run individual tests
  • Check code coverage
  • View console output

Error Debugging

Detailed Error Messages

test("improve error message", async () => {
  const userId = 999;

  // Basic error
  expect(await UserModel.findById(userId)).toBeDefined();

  // Detailed error message
  const user = await UserModel.findById(userId);
  expect(user, `User ID ${userId} not found`).toBeDefined();
});

Checking Errors with try-catch

test("error detail analysis", async () => {
  try {
    await riskyOperation();
    expect.fail("Error should have occurred");
  } catch (error) {
    // Check error details
    console.log("Error type:", error.constructor.name);
    console.log("Error message:", error.message);
    console.log("Stack trace:", error.stack);

    expect(error).toBeInstanceOf(BadRequestException);
  }
});

Debugging Async Code

Checking async/await

test("debugging async operations", async () => {
  console.log("1. Started");

  const promise1 = fetchUser(1);
  console.log("2. fetchUser called");

  const promise2 = fetchOrders(1);
  console.log("3. fetchOrders called");

  const [user, orders] = await Promise.all([promise1, promise2]);
  console.log("4. All completed:", { user, orders });

  expect(user).toBeDefined();
});

Increasing Timeout

test("slow operation", async () => {
  // Increase from default 5 seconds to 30 seconds
  const result = await slowOperation();
  expect(result).toBeDefined();
}, 30000);  // 30 second timeout

Debugging Mocks

Checking Mock Calls

import { vi } from "vitest";

test("debugging mock calls", async () => {
  const mockFn = vi.fn().mockResolvedValue({ id: 1 });

  await mockFn("test");
  await mockFn("test2");

  // Check mock calls
  console.log("Call count:", mockFn.mock.calls.length);
  console.log("First call:", mockFn.mock.calls[0]);
  console.log("All calls:", mockFn.mock.calls);

  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith("test");
});

Checking Mock Reset

beforeEach(() => {
  // Reset mocks before each test
  vi.clearAllMocks();
});

test("check mock state", () => {
  const mockFn = vi.fn();

  // Before call
  console.log("Before call:", mockFn.mock.calls.length); // 0

  mockFn("test");

  // After call
  console.log("After call:", mockFn.mock.calls.length); // 1
});

Checking Database State

Query Logging

test("check DB query", async () => {
  // Enable query logging (during development)
  const result = await UserModel.getPuri()
    .select("*")
    .where({ email: "test@example.com" })
    .debug()  // Output query
    .getOne();

  console.log("Result:", result);
});

Checking Data After Test

test("verify data creation", async () => {
  const email = "test@example.com";

  await UserModel.create({ email });

  // Check directly from DB
  const users = await UserModel.getPuri()
    .select("*")
    .getMany();

  console.log("All users:", users);

  const created = users.find(u => u.email === email);
  console.log("Created user:", created);

  expect(created).toBeDefined();
});

Coverage Debugging

Checking Coverage

# Generate coverage
pnpm test --coverage

# Open HTML report
open coverage/index.html

Finding Uncovered Code

// src/models/user.model.ts
class UserModelClass extends BaseModel {
  async createUser(email: string) {
    if (!email) {
      // This branch is not tested
      throw new Error("Email required");
    }

    return this.save({ email });
  }
}

// Add test
test("error when creating without email", async () => {
  await expect(
    UserModel.createUser("")
  ).rejects.toThrow("Email required");
});

Vitest Configuration Debugging

Checking Configuration

vitest.config.ts:
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    include: ["src/**/*.test.ts"],

    // Debugging options
    bail: 1,  // Stop on first failure
    reporters: ["verbose"],  // Verbose output
    logHeapUsage: true,  // Output memory usage

    // Increase timeouts
    testTimeout: 10000,
    hookTimeout: 10000
  }
});

Checking Environment Variables

test("debugging environment variables", () => {
  console.log("NODE_ENV:", process.env.NODE_ENV);
  console.log("DB_HOST:", process.env.DB_HOST);
  console.log("All env vars:", process.env);
});

Debugging Parallel Execution

Disabling Isolation Mode

Some tests may have issues when running in parallel:
// vitest.config.ts
export default defineConfig({
  test: {
    pool: "forks",
    maxWorkers: 1,  // Single worker
    isolate: false  // Disable isolation
  }
});

Sequential Execution

# Sequential execution
pnpm vitest run --no-threads

Snapshot Debugging

Updating Snapshots

# Update snapshots
pnpm vitest run -u

# Specific file only
pnpm vitest run -u src/models/user.model.test.ts

Checking Snapshots

test("snapshot debugging", () => {
  const data = {
    id: 1,
    name: "Test",
    timestamp: new Date().toISOString()
  };

  // timestamp keeps changing
  expect(data).toMatchSnapshot();

  // Exclude dynamic values
  expect({
    ...data,
    timestamp: expect.any(String)
  }).toMatchSnapshot();
});

Type Check Debugging

Checking Type Errors

# Run type check only
pnpm vitest typecheck

# Or
pnpm tsc --noEmit

Type Tests

// src/**/*type-safety.test.ts
import { expectTypeOf } from "vitest";

test("verify type safety", () => {
  const user = { id: 1, email: "test@example.com" };

  expectTypeOf(user.id).toEqualTypeOf<number>();
  expectTypeOf(user.email).toEqualTypeOf<string>();
});

Test Isolation Issues

Checking beforeEach/afterEach

describe("User Tests", () => {
  let testUser: User;

  beforeEach(async () => {
    console.log("Setup started");
    testUser = await UserModel.create({
      email: "test@example.com"
    });
    console.log("Setup completed:", testUser.id);
  });

  afterEach(async () => {
    console.log("Cleanup started");
    if (testUser) {
      await UserModel.del(testUser.id);
    }
    console.log("Cleanup completed");
  });

  test("test 1", async () => {
    expect(testUser).toBeDefined();
  });
});

Memory Leak Debugging

Checking Memory Usage

// vitest.config.ts
export default defineConfig({
  test: {
    logHeapUsage: true  // Output memory usage
  }
});

Checking Resource Cleanup

test("verify resource cleanup", async () => {
  const db = createConnection();

  try {
    await db.query("SELECT 1");
  } finally {
    await db.destroy();  // Always cleanup
    console.log("DB connection cleanup completed");
  }
});

Best Practices

1. Test in Small Units

// Too large test
test("entire user flow", async () => {
  // Create, read, update, delete all included
});

// Split into small units
test("create user", async () => { });
test("get user", async () => { });
test("update user", async () => { });
test("delete user", async () => { });

2. Meaningful Test Names

// Unclear name
test("test1", async () => { });

// Clear name
test("BadRequestException when creating user without email", async () => {
  await expect(
    UserModel.create({ email: "" })
  ).rejects.toThrow(BadRequestException);
});

3. AAA Pattern (Arrange-Act-Assert)

test("create user", async () => {
  // Arrange
  const email = "test@example.com";
  const name = "Test User";

  // Act
  const user = await UserModel.create({ email, name });

  // Assert
  expect(user.email).toBe(email);
  expect(user.name).toBe(name);
});

4. Sufficient Information on Failure

test("verify complex conditions", async () => {
  const result = await complexCalculation();

  // Make clear what went wrong on failure
  expect(
    result.total,
    `total should be ${result.expected} but was ${result.total}`
  ).toBe(result.expected);
});