Unit Testing

Unit testing in Bascik focuses on verifying pure functions, utility modules, data transformers, and business logic in isolation. Because Node 24 and Node 22.18+ natively execute TypeScript files by erasing type annotations, Vitest executes unit tests directly against .ts files with zero build delay.

Setting Up Vitest

If your project was scaffolded with create-bascik, Vitest is pre-configured. To set up Vitest manually in an existing project, install the dependencies:

sh
npm install -D vitest @vitest/coverage-v8

Create a vite.config.js file in your project root:

js
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    include: ['src/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Add test commands to your package.json:

json
"scripts": {
  "test": "vitest run",
  "test:watch": "vitest",
  "test:coverage": "vitest run --coverage"
}

Writing High-Value Unit Tests

Focus unit tests on core business logic, algorithm correctness, and edge-case handling rather than trivial property checks.

Example: Testing Pure Calculation Functions

ts
// src/utils/formatters.ts
export function formatCurrency(cents: number, currency = 'USD'): string {
  if (isNaN(cents) || cents < 0) return '$0.00';
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(cents / 100);
}
ts
// src/utils/formatters.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './formatters.ts';

describe('formatCurrency', () => {
  it('formats positive cent values correctly', () => {
    expect(formatCurrency(1999)).toBe('$19.99');
    expect(formatCurrency(500)).toBe('$5.00');
  });

  it('handles zero and invalid inputs gracefully', () => {
    expect(formatCurrency(0)).toBe('$0.00');
    expect(formatCurrency(-500)).toBe('$0.00');
    expect(formatCurrency(NaN)).toBe('$0.00');
  });
});

Testing API Route Handlers

Because Bascik API routes use the web standard Request and Response contract, you can unit test handlers directly by passing a Request object without spinning up a live network server or using heavy mock libraries:

ts
// src/api/contact.test.ts
import { describe, it, expect } from 'vitest';
import { POST } from './contact.ts';

describe('POST /api/contact', () => {
  it('creates contact message successfully', async () => {
    const request = new Request('http://localhost/api/contact', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ name: 'Alice', email: '[email protected]' }),
    });

    const response = await POST(request, { params: {}, remoteIp: '127.0.0.1' });
    expect(response.status).toBe(201);

    const data = await response.json();
    expect(data.ok).toBe(true);
  });
});

Property-Based Testing with fast-check

For complex parsers, string token replacers, and mathematical transforms, use property-based testing (fast-check) to generate thousands of randomized inputs and verify system invariants across all edge cases:

sh
npm install -D fast-check
ts
// src/utils/slugify.test.ts
import { describe, it, expect } from 'vitest';
import fc from 'fast-check';

function slugify(input: string): string {
  return input
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
}

describe('slugify property invariants', () => {
  it('never outputs spaces or uppercase characters', () => {
    fc.assert(
      fc.property(fc.string(), (str) => {
        const slug = slugify(str);
        expect(slug).not.toMatch(/\s/);
        expect(slug).toBe(slug.toLowerCase());
      })
    );
  });
});

V8 Code Coverage Reports

Running npm run test:coverage generates comprehensive code coverage metrics powered by @vitest/coverage-v8:

  • Terminal Summary: Displays statement, branch, function, and line coverage percentages directly in stdout.
  • Interactive HTML Report: Generated in coverage/index.html for line-by-line inspection of untested paths in any browser.
  • CI Artifacts: Saved in coverage/coverage-final.json for integration into automated CI/CD pipelines.

Tip: Add coverage/ to your .gitignore file to prevent committing generated coverage artifacts to version control.