Skip to content

Testing Guide ​

Comprehensive testing guide for clear-router

Table of Contents ​

Test Setup ​

Prerequisites ​

bash
pnpm install

This will install all required dependencies including:

  • vitest (test framework)
  • parasito (HTTP testing)
  • typescript & ts-node (TypeScript support)

Configuration Files ​

vitest.config.js ​

javascript
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],

  test: {
    retry: 10,
    root: './',
    passWithNoTests: true,
    environment: 'node',
    include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)'],
    env: {
      NODE_ENV: 'test',
    },
    coverage: {
      reporter: ['text', 'json', 'html', 'lcov'],
      reportsDirectory: 'coverage',
      exclude: [
        '**/node_modules/**',
        '**/dist/**',
        '**/cypress/**',
        '**/.{idea,git,cache,output,temp}/**',
        '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*',
        '**/.h3ravel/**',
      ],
    },
  },
});

tsconfig.json ​

json
{
  "compilerOptions": {
    "module": "es2022",
    "target": "es2022",
    "lib": ["es2022", "DOM"],
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./",
    "types": ["node", "vitest", "express"],
    "baseUrl": ".",
    "paths": {
      "clear-router/express": ["src/express/routes.ts"],
      "clear-router/express/*": ["src/express/*"],
      "clear-router/h3": ["src/h3/routes.ts"],
      "clear-router/h3/*": ["src/h3/*"]
    }
  },
  "include": ["tests/**/*", "types/**/*", "example/**/*"],
  "exclude": ["node_modules"]
}

Running Tests ​

All Tests ​

bash
pnpm test

This runs all test suites: CommonJS, ESM, and TypeScript.

Individual Test Suites ​

bash
# ESM only
pnpm run test:esm

# TypeScript only
pnpm run test:ts

Watch Mode ​

bash
# Watch ESM tests
npx vitest --watch tests/esm.test.ts

# Watch TypeScript tests
npx vitest --watch tests/typescript.test.ts

Coverage ​

bash
npx vitest --coverage

Test Structure ​

All tests follow a similar structure testing:

  1. Basic route registration (GET, POST, PUT, DELETE, PATCH)
  2. Route grouping
  3. Middleware application (global, group, route-level)
  4. Controller bindings (static and instance)
  5. Error handling
  6. Route inspection with allRoutes()
  7. Controller instance request hydration (body, query, params, clearRequest)
  8. Handler argument contract (context as first arg, clearRequest as second arg)

CommonJS Tests ​

File: tests/commonjs.test.js

Example Test ​

javascript
const request = require('parasito');
const express = require('express');
const Routes = require('../src/routes');

describe('Express Routing - CommonJS', () => {
  let app;
  let router;

  beforeEach(() => {
    // Reset routes before each test
    Router.reset();

    app = express();
    router = express.Router();
    app.use(express.json());
  });

  afterEach(() => {
    Router.apply(router);
    app.use(router);
  });

  test('should register and handle GET route', async () => {
    Router.get('/test', ({ res }) => {
      res.json({ message: 'success' });
    });

    const response = await request(app).get('/test');
    expect(response.status).toBe(200);
    expect(response.body.message).toBe('success');
  });

  test('should apply middleware correctly', async () => {
    const middleware = (req, res, next) => {
      req.customValue = 'middleware-applied';
      next();
    };

    Router.get(
      '/middleware-test',
      ({ req, res }) => {
        res.json({ value: req.customValue });
      },
      [middleware],
    );

    const response = await request(app).get('/middleware-test');
    expect(response.body.value).toBe('middleware-applied');
  });

  test('should handle route groups', async () => {
    Router.group('/api', () => {
      Router.get('/users', ({ res }) => {
        res.json({ route: 'users' });
      });
    });

    const response = await request(app).get('/api/users');
    expect(response.status).toBe(200);
    expect(response.body.route).toBe('users');
  });

  test('should bind controller methods', async () => {
    class TestController {
      static index({ res }) {
        res.json({ controller: 'static' });
      }
    }

    Router.get('/controller', [TestController, 'index']);

    const response = await request(app).get('/controller');
    expect(response.body.controller).toBe('static');
  });

  test('should handle errors properly', async () => {
    Router.get('/error', () => {
      throw new Error('Test error');
    });

    app.use((err, req, res, next) => {
      res.status(500).json({ error: err.message });
    });

    const response = await request(app).get('/error');
    expect(response.status).toBe(500);
    expect(response.body.error).toBe('Test error');
  });

  test('should return all routes info', () => {
    Router.get('/route1', ({ res }) => res.send('ok'));
    Router.post('/route2', ({ res }) => res.send('ok'));

    const allRoutes = Router.allRoutes();
    expect(allRoutes).toHaveLength(2);
    expect(allRoutes[0].methods).toContain('get');
    expect(allRoutes[0].path).toBe('/route1');
    expect(allRoutes[1].methods).toContain('post');
  });
});

ESM Tests ​

File: tests/esm.test.mjs

Example Test ​

javascript
import request from 'parasito';
import express from 'express';
import Router from '../src/routes.mjs';

describe('Express Routing - ESM', () => {
  let app;
  let router;

  beforeEach(() => {
    Router.reset();

    app = express();
    router = express.Router();
    app.use(express.json());
  });

  afterEach(async () => {
    await Router.apply(router);
    app.use(router);
  });

  test('should register GET route with ESM', async () => {
    Router.get('/esm-test', ({ res }) => {
      res.json({ module: 'esm' });
    });

    const response = await request(app).get('/esm-test');
    expect(response.status).toBe(200);
    expect(response.body.module).toBe('esm');
  });

  test('should handle async handlers', async () => {
    Router.get('/async', async ({ res }) => {
      await new Promise((resolve) => setTimeout(resolve, 10));
      res.json({ async: true });
    });

    const response = await request(app).get('/async');
    expect(response.body.async).toBe(true);
  });

  test('should support multiple methods', async () => {
    Router.add(['get', 'post'], '/multi', ({ req, res }) => {
      res.json({ method: req.method });
    });

    const getResponse = await request(app).get('/multi');
    expect(getResponse.body.method).toBe('GET');

    const postResponse = await request(app).post('/multi');
    expect(postResponse.body.method).toBe('POST');
  });
});

Running ESM Tests ​

ESM tests require special Node.js flags:

bash
NODE_OPTIONS=--experimental-vm-modules npx vitest tests/esm.test.ts

Or use the pnpm script:

bash
pnpm run test:esm

TypeScript Tests ​

File: tests/typescript.test.ts

Example Test ​

typescript
import request from 'parasito';
import express, { Router } from 'express';
import Router, { HttpContext } from '../types/index';

describe('Express Routing - TypeScript', () => {
  let app: express.Application;
  let router: Router;

  beforeEach(() => {
    Router.reset();

    app = express();
    router = Router();
    app.use(express.json());
  });

  afterEach(async () => {
    await Router.apply(router);
    app.use(router);
  });

  test('should work with TypeScript types', async () => {
    Router.get('/typescript', ({ req, res }: HttpContext) => {
      res.json({ typed: true });
    });

    const response = await request(app).get('/typescript');
    expect(response.body.typed).toBe(true);
  });

  test('should type check route info', () => {
    Router.get('/info', ({ res }: HttpContext) => res.send('ok'));

    const routes = Router.allRoutes();
    expect(routes[0].path).toBe('/info');
    expect(routes[0].handlerType).toBe('function');
  });

  test('should type check controller', async () => {
    class UserController {
      static index({ res }: HttpContext): void {
        res.json({ users: [] });
      }
    }

    Router.get('/users', [UserController, 'index']);

    const response = await request(app).get('/users');
    expect(response.body.users).toEqual([]);
  });

  test('should handle typed middleware', async () => {
    const authMiddleware = (
      req: express.Request,
      res: express.Response,
      next: express.NextFunction,
    ) => {
      req.body.authenticated = true;
      next();
    };

    Router.post(
      '/auth',
      ({ req, res }: HttpContext) => {
        res.json({ auth: req.body.authenticated });
      },
      [authMiddleware],
    );

    const response = await request(app).post('/auth');
    expect(response.body.auth).toBe(true);
  });
});

Running TypeScript Tests ​

bash
pnpm run test:ts

Or directly:

bash
npx ts-node tests/typescript.test.ts

Writing Custom Tests ​

Test Template ​

javascript
const request = require('parasito');
const express = require('express');
const Routes = require('clear-router');

describe('My Custom Tests', () => {
  let app;
  let router;

  beforeEach(() => {
    // Reset Routes state
    Router.reset();

    // Create fresh Express app and router
    app = express();
    router = express.Router();
    app.use(express.json());
  });

  afterEach(async () => {
    // Apply routes after each test
    await Router.apply(router);
    app.use(router);
  });

  test('your test description', async () => {
    // Define routes
    Router.get('/test', ({ res }) => {
      res.json({ success: true });
    });

    // Make request
    const response = await request(app).get('/test');

    // Assert
    expect(response.status).toBe(200);
    expect(response.body.success).toBe(true);
  });
});

Testing Best Practices ​

  1. Always reset Routes state before each test to avoid conflicts
  2. Use beforeEach and afterEach for setup and teardown
  3. Test both success and error cases
  4. Test middleware execution order
  5. Test edge cases (empty paths, invalid handlers, etc.)
  6. Use descriptive test names

Example: Testing Middleware Order ​

javascript
test('should execute middlewares in correct order', async () => {
  const order = [];

  const mw1 = (req, res, next) => {
    order.push('global');
    next();
  };

  const mw2 = (req, res, next) => {
    order.push('group');
    next();
  };

  const mw3 = (req, res, next) => {
    order.push('route');
    next();
  };

  Router.middleware([mw1], () => {
    Router.group(
      '/api',
      () => {
        Router.get(
          '/test',
          ({ res }) => {
            res.json({ order });
          },
          [mw3],
        );
      },
      [mw2],
    );
  });

  const response = await request(app).get('/api/test');
  expect(response.body.order).toEqual(['global', 'group', 'route']);
});

Example: Testing Error Handling ​

javascript
test('should pass errors to Express error handler', async () => {
  Router.get('/error', () => {
    throw new Error('Custom error');
  });

  app.use((err, req, res, next) => {
    res.status(500).json({ error: err.message });
  });

  const response = await request(app).get('/error');
  expect(response.status).toBe(500);
  expect(response.body.error).toBe('Custom error');
});

Continuous Integration ​

GitHub Actions Example ​

yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x, 20.x]

    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      - run: pnpm ci
      - run: pnpm test
      - run: pnpm run build

Troubleshooting ​

ESM Tests Not Running ​

Make sure you're using the correct Node.js flags:

bash
NODE_OPTIONS=--experimental-vm-modules npx vitest tests/esm.test.ts

TypeScript Type Errors ​

Ensure you have proper TypeScript configuration:

bash
pnpm install --save-dev typescript @types/node @types/express

Test Isolation Issues ​

If tests are affecting each other, make sure you're resetting Routes state:

javascript
beforeEach(() => {
  Router.reset();
});

Additional Resources ​