qtests
A comprehensive Node.js testing framework with zero dependencies. Provides intelligent test generation, method stubbing, console mocking, and drop-in replacements for popular modules. Now with ES Module and TypeScript support!
🎉 Latest Updates (September 2025):
- ✅ ESM + TypeScript Jest harness: runner always loads
config/jest.config.mjsand passes--passWithNoTestsfor stable CI - ✅ HTTP testing shim alignment: TS shim re-exports a working JS shim with chainable
.send()and properreq.body - ✅ Safe Mongoose mocking: Jest
moduleNameMappermapsmongooseto qtests' manual mock (no real DB access) - ✅ Performance optimized: Jest-like batch execution with 69% speed improvement
- ✅ Enhanced test generation: Smarter filtering, React-aware scaffolds, and safe defaults
🚀 Quick Start
npm install qtests --save-dev
qtests passively scaffolds its runner at your project root after install (via npm postinstall). No extra steps required.
Configure your project for ES modules by adding to package.json:
{
"type": "module",
"main": "index.ts"
}
TypeScript setup:
// Enable automatic stubbing
import './node_modules/qtests/setup.js';
// Your modules now use qtests stubs automatically
import axios from 'axios'; // Uses qtests stub
import winston from 'winston'; // Uses qtests stub
// Import with full type safety
import { stubMethod, mockConsole, testEnv, QtestsAPI } from 'qtests';
// Use with TypeScript intellisense
const restore = stubMethod(myObject, 'methodName', mockImplementation);
✨ Key Features
- 🤖 Intelligent Test Generation - Automatically discovers and generates missing tests
- 🎭 Method Stubbing - Temporarily replace object methods with automatic restoration
- 📺 Console Mocking - Jest-compatible console spies with fallback for vanilla Node.js
- 🌍 Environment Management - Safe backup and restore of environment variables
- 📦 Module Stubs - Drop-in replacements for axios, winston, and other dependencies
- 🔌 Offline Mode - Automatic stub resolution when external services are unavailable
- 🏃 Lightweight Test Runner - Zero-dependency test execution engine
- 🌐 HTTP Testing - Integration testing utilities (supertest alternative)
- 📧 Email Mocking - Email testing without external mail services
- 🆕 ES Module Support - Full compatibility with modern ES Module syntax
- 🔷 TypeScript Support - Complete type definitions and intellisense
- ⚡ Zero Dependencies - No production dependencies to bloat your project
🧩 Mock API (Runtime‑Safe)
qtests exposes a small, extensible mocking API that works at runtime without rewriting paths or adding heavy frameworks.
Defaults registered by setup:
axios→ qtests stub (truthy, no network)winston→ qtests stub (no‑op logger with format/transports)mongoose→ project__mocks__/mongoose.jsif present, or a minimal safe object
Usage:
import qtests from 'qtests';
// Register a custom module mock
qtests.mock.module('external-service', () => ({
default: {
call: async () => ({ ok: true })
}
}));
// Now `require('external-service')` or `import ... from 'external-service'` returns the mock (CJS via require hook; ESM early via optional loader)
Notes:
- Activation is runtime‑safe: a single require hook returns registered mocks; previously loaded CJS modules are best‑effort evicted from
require.cache. - ESM projects can optionally use the loader for earliest interception:
node --loader=qtests/loader.mjs your-app.mjs
- setup still runs first in Jest via
config/jest-setup.tsso defaults are active before imports.
📖 Core Usage
Method Stubbing
import { stubMethod } from 'qtests';
const myObj = { greet: (name: string) => `Hello, ${name}!` };
// Stub the method
const restore = stubMethod(myObj, 'greet', () => 'Hi!');
console.log(myObj.greet('Brian')); // 'Hi!'
// Restore original
restore();
console.log(myObj.greet('Brian')); // 'Hello, Brian!'
Console Mocking
import { mockConsole } from 'qtests';
const spy = mockConsole('log');
console.log('test message');
console.log(spy.mock.calls); // [['test message']]
spy.mockRestore(); // Restore original console.log
Environment Management
import { testEnv } from 'qtests';
// Set test environment
testEnv.setTestEnv(); // Sets NODE_ENV=test, DEBUG=qtests:*
// Save and restore environment
const saved = testEnv.saveEnv();
process.env.TEST_VAR = 'modified';
testEnv.restoreEnv(saved); // TEST_VAR removed, original state restored
🧪 Unified Test Runner (API‑Only)
- One command for everyone:
npm test. - One runner:
qtests-runner.mjsruns Jest via the programmatic APIrunCLI(no child processes, notsx). - Honors:
QTESTS_INBAND=1(serial) andQTESTS_FILE_WORKERS=<n>(max workers). - Always uses project config and
passWithNoTests, withcache=trueandcoverage=false. - Debugging: creates
DEBUG_TESTS.mdon failures; override withQTESTS_DEBUG_FILE=pathor suppress withQTESTS_SUPPRESS_DEBUG=1.
Runner availability and generator behavior:
- Postinstall scaffolding automatically creates
qtests-runner.mjsat the project root (INIT_CWD) when missing. npx qtests-generateALWAYS (re)writesqtests-runner.mjsat the client root to keep the runner current.- Scaffolds
config/jest.config.mjs(ignoresdist/,build/) andconfig/jest-require-polyfill.cjs(ensuresrequire(...)is available in ESM tests). - Scaffolds
qtests-runner.mjs(API‑only runner). - Ensures helper scripts exist:
scripts/clean-dist.mjsandscripts/ensure-runner.mjs. - Updates
package.jsonscripts to:pretest:node scripts/clean-dist.mjs && node scripts/ensure-runner.mjstest:node qtests-runner.mjs
Stale runner protection:
scripts/ensure-runner.mjssilently replaces stale runners (e.g., spawn/parallel-mode or missing API‑only invariants) with the validated template.
Migration (from spawn‑based runners):
- Run
npx qtests-generateonce to update the runner and scripts. - Ensure package.json contains the
pretestandtestcommands above. - Remove any custom
tsx/spawn‑based test commands.
CI verification:
npm run ci:verifyvalidates runner policy, script wiring, dist hygiene, and Jest config.
🤖 Automatic Test Generation
CLI Usage (TypeScript ESM)
# Generate tests for entire project
npx qtests-generate
# Custom source directory
npx qtests-generate --src lib
# Custom source and test directories
npx qtests-generate --src app --test-dir tests/integration
# Only unit tests, preview without writing
npx qtests-generate --unit --dry-run
# Restrict to TypeScript files and skip existing tests
npx qtests-generate --include "**/*.ts" --exclude "**/*.test.ts"
# Use AST mode (requires typescript) and allow overwrites of generated tests
npx qtests-generate --mode ast --force
# Force React mode and add router wrapper
npx qtests-generate --react --with-router
# Backward-compatible alias
# (if your environment still references the old name)
npx qtests-ts-generate
Programmatic Usage (TypeScript ESM)
import { TestGenerator } from 'qtests';
const generator = new TestGenerator({
SRC_DIR: 'src',
TEST_DIR: 'tests/integration',
include: ['**/*.ts'],
exclude: ['**/*.test.ts'],
mode: 'heuristic', // or 'ast' (requires `typescript`), falls back gracefully
});
await generator.generateTestFiles(false); // pass true for dry-run
const results = generator.getResults();
console.log(`Generated ${results.length} test files`);
Smart Discovery Features:
- Walks entire project directory structure
- Supports feature-first projects (tests alongside source files)
- Detects existing tests to avoid duplicates
- Handles multiple project structures (traditional, monorepo, mixed)
🔌 Module Stubs
Axios Stub
// Automatic when using qtests/setup
import axios from 'axios';
const response = await axios.get('/api');
// Returns: { data: {}, status: 200, statusText: 'OK', headers: {}, config: {} }
await axios.post('/api', data); // Enhanced response format
Winston Stub
// Automatic when using qtests/setup
import winston from 'winston';
const logger = winston.createLogger();
logger.info('This produces no output'); // Silent
Custom Module Stubs (Ad‑Hoc)
When you need to stub a niche dependency (beyond the built‑ins axios/winston) without changing qtests itself, register a custom stub in tests:
// Always load setup first so axios/winston are stubbed globally
import './node_modules/qtests/setup.js';
// Then register your ad‑hoc stub(s)
import { registerModuleStub } from 'qtests/utils/customStubs.js';
registerModuleStub('external-service-client', {
ping: () => 'pong',
get: async () => ({ ok: true })
});
// Now this resolves to your in‑memory stub even if the module is not installed
const client = require('external-service-client');
await client.get(); // { ok: true }
Notes:
- Call
registerModuleStubBEFORE the first require/import of that module. - Use
unregisterModuleStub(id)andclearAllModuleStubs()for cleanup in afterEach. - Honors
QTESTS_SILENT=1|trueto reduce noise in CI logs.
🏃 Lightweight Test Runner
import { runTestSuite, createAssertions } from 'qtests';
const assert = createAssertions();
const tests = {
'basic test': () => {
assert.equals(1 + 1, 2);
assert.isTrue(true);
},
'async test': async () => {
const result = await Promise.resolve('done');
assert.equals(result, 'done');
}
};
runTestSuite('My Tests', tests);
🌐 HTTP Testing
// For generated API tests, a local shim is scaffolded at:
// tests/generated-tests/utils/httpTest.ts (re-exports a JS shim)
// tests/generated-tests/utils/httpTest.shim.js (implementation with .send())
// You can also import the same helpers directly from qtests if preferred.
import { httpTest } from 'qtests/lib/envUtils.js';
// Create mock Express app
const app = httpTest.createMockApp();
app.get('/users', (req, res) => {
res.statusCode = 200;
res.end(JSON.stringify({ users: [] }));
});
// Test the app — chainable .send() supported; JSON is defaulted and parsed
const response = await httpTest.supertest(app)
.get('/users')
.expect(200)
.end();
📧 Email Testing
import { sendEmail } from 'qtests/lib/envUtils.js';
// Mock email sending
const result = await sendEmail.send({
to: 'user@example.com',
subject: 'Welcome',
text: 'Welcome to our app!'
});
console.log(result.success); // true
console.log(sendEmail.getHistory()); // Array of sent emails
🛠️ Advanced Features
Offline Mode
import { offlineMode } from 'qtests';
// Enable offline mode
offlineMode.setOfflineMode(true);
// Get stubbed axios automatically
const axios = offlineMode.getAxios();
await axios.get('/api/data'); // Returns {} instead of real request
Integration with Jest
import { testHelpers } from 'qtests';
test('console output', async () => {
await testHelpers.withMockConsole('log', (spy) => {
console.log('test');
expect(spy.mock.calls[0][0]).toBe('test');
});
});
📚 API Reference
Core Methods
| Method | Description |
|---|---|
stubMethod(obj, methodName, replacement) |
Replace object method with stub |
mockConsole(method) |
Mock console methods with spy |
testEnv.setTestEnv() |
Set standard test environment |
testEnv.saveEnv() / restoreEnv() |
Backup/restore environment |
offlineMode.setOfflineMode(enabled) |
Toggle offline mode |
Test Generation
| Method | Description |
|---|---|
new TestGenerator(options) |
Create test generator instance |
generator.generateTestFiles(dryRun?) |
Generate missing tests (dryRun optional) |
generator.getResults() |
Get list of generated files |
CLI: npx qtests-generate |
Command-line test generation (alias: qtests-ts-generate) |
Test Runner
| Method | Description |
|---|---|
runTestSuite(name, tests) |
Execute test suite |
createAssertions() |
Get assertion methods |
🔷 TypeScript Configuration
To use qtests with ES modules and TypeScript, update your package.json:
{
"type": "module",
"main": "index.ts"
}
And ensure your tsconfig.json supports ES modules:
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"ts-node": {
"esm": true
}
}
Import Patterns
// Core utilities with full type safety
import { stubMethod, mockConsole, testEnv, TestGenerator, QtestsAPI } from 'qtests';
// Advanced utilities
import { httpTest, sendEmail, testSuite } from 'qtests/lib/envUtils.js';
// Module stubs
import { stubs } from 'qtests';
await stubs.axios.get('https://example.com');
🎯 Best Practices
🧰 CLI Reference
qtests-generate (alias: qtests-ts-generate)
- Usage:
qtests-generate [options] - Purpose: Scans source files and generates missing tests.
- Options:
-s, --src <dir>: Source directory root to scan. Default:.-t, --test-dir <dir>: Directory for integration/API tests. Default:tests/generated-tests--mode <heuristic|ast>: Analysis mode.astattempts TypeScript-based analysis iftypescriptis installed; falls back otherwise. Default:heuristic--unit: Generate only unit tests--integration: Generate only integration/API tests--include <glob>: Include only matching files (repeatable)--exclude <glob>: Exclude matching files (repeatable)--dry-run: Preview actions; no files written or package.json updates
--force: Overwrite generated test files (filenames containing.GeneratedTestor legacy.GenerateTest)--react: Force React mode (use jsdom, React templates)--with-router: Wrap React tests with MemoryRouter when React Router is detected--react-components: Opt-in to generating tests for React components--no-react-components: Skip generating tests for React components (default)-h, --help: Show help-v, --version: Show version
Examples:
qtests-generate— scan current directory with defaultsqtests-generate --src lib— scanlibonlyqtests-generate --unit --dry-run— preview unit tests onlyqtests-generate --include "**/*.ts" --exclude "**/*.test.ts"— filter filesqtests-generate --mode ast --force— AST mode and overwrite generated tests
Notes:
- On real runs (no
--dry-run), the generator writesconfig/jest.config.mjs,config/jest-setup.ts, and createsqtests-runner.mjs. - The generated runner includes
--config config/jest.config.mjsand--passWithNoTests. - The generated Jest config includes a
moduleNameMapperformongoosepointing to qtests' manual mock, preventing real DB access in unit tests. - Update of
package.jsontest script is now opt-in via--update-pkg-script. - In
--dry-run, none of the above files are written. - Enhanced file filtering automatically skips demo/, examples/, config/, and test utility directories.
React/Hook Templates and Providers
- Components: By default, component test generation is disabled to reduce noise. Opt-in with
--react-components. When enabled, components get a smoke render viaReact.createElement(Component, {}), asserting container exists only. - Hooks: Uses a probe component to mount the hook; avoids invalid direct calls.
- Providers: If
@tanstack/react-queryis imported, renders insideQueryClientProvider. Ifreact-hook-formis detected (oruseFormContext/FormProvideris referenced), wraps withFormProviderusinguseForm(). - Optional Router: With
--with-routerand when source importsreact-router(-dom), wraps withMemoryRouter. - Required-props fallback: If a component appears to require props (TS inline types or propTypes.isRequired), generator falls back to a safe existence test instead of rendering.
- Non-React modules: Emits safe existence checks or a module-load smoke test.
- Skipped directories:
__mocks__,__tests__,tests,test,generated-tests,manual-tests,node_modules,dist,build,.git. - API tests: Local
tests/generated-tests/utils/httpTest.tsis scaffolded to re-exporthttpTest.shim.js, a minimal, dependency‑free HTTP test shim. Imports like../utils/httpTestresolve without extra project config. The shim supports.send()and exposesreq.bodyto handlers.
File Extension Strategy & JSX
- Tests are emitted JSX-free using
React.createElement, so unit/API tests default to.ts. .tsxis only chosen when the generated test includes JSX (rare; currently templates avoid JSX).
Safety + Sanity Filters
- Export filtering removes reserved/falsy/non-identifiers (e.g.,
default,function,undefined). - If no safe export remains, the generator emits a module smoke test instead of bogus per-export tests.
- When valid React component/hook tests are emitted, the generator does not append generic “is defined” blocks.
qtests runner
- Usage:
qtests-ts-runner - Purpose: Discovers and runs tests in the project with a Jest-first strategy.
- Behavior:
- Discovers files matching
.test|.spec|_test|_specwith.js|.ts|.jsx|.tsx - Tries
npx jestwith fast flags; falls back to verbose; finally runs withnodeif needed - Runs tests in parallel batches (2x CPU cores, capped by file count)
- Performance Optimized: Jest-like batch execution achieving 69% speed improvement
- Discovers files matching
- Notes:
- Automatically generated as
qtests-runner.mjsby the test generator - Always passes
--config config/jest.config.mjsand--passWithNoTests - Honors
QTESTS_SUPPRESS_DEBUG=1|trueto skip creatingDEBUG_TESTS.md - Honors
QTESTS_DEBUG_FILEto set a custom debug report path/name - Records Jest argv to
runner-jest-args.jsonto aid debugging - Works with TypeScript ESM projects via
ts-jest(scaffolded by the generator)
- Automatically generated as
1. Always Load Setup First
// ✅ Correct
import './node_modules/qtests/setup.js';
import myModule from './myModule.js';
// ❌ Wrong
import myModule from './myModule.js';
import './node_modules/qtests/setup.js';
2. Clean Up After Tests
test('example', () => {
const restore = stubMethod(obj, 'method', stub);
const spy = mockConsole('log');
// ... test code ...
// Always restore
restore();
spy.mockRestore();
});
3. Use Environment Helpers
import { testHelpers } from 'qtests';
test('environment test', async () => {
await testHelpers.withSavedEnv(async () => {
process.env.TEST_VAR = 'value';
// Environment automatically restored
});
});
🐛 Troubleshooting
| Issue | Solution |
|---|---|
| Stubs not working (CommonJS) | Ensure require('qtests/setup') is called first |
| Stubs not working (ES Modules) | Ensure import './node_modules/qtests/setup.js' is called first |
| TypeScript import errors | Add "type": "module" to package.json and update tsconfig.json |
| ES Module syntax errors | Ensure "module": "ES2020" in tsconfig.json |
| Console pollution | Use mockConsole() to capture output |
| Environment leaks | Use testHelpers.withSavedEnv() for isolation |
| Module not found | Import advanced utilities from qtests/lib/envUtils |
| CLI not found | Use npx qtests-generate (alias: qtests-ts-generate) or install globally |
| File extension errors | Use .js extensions in ES module imports |
| Test generation creates tests for config files | Enhanced filtering now automatically skips demo/, examples/, config/, and test directories |
| generateKey returns empty string | Fixed in latest version - now correctly returns test keys like "test-api-key-user" |
| qtests-runner.mjs vs qtests-ts-runner.ts | Use qtests-runner.mjs as the generated runner; the CLI command remains qtests-ts-runner |
📄 License
MIT License - see LICENSE file for details.
🤝 Contributing
Contributions welcome! Please see our contributing guidelines and feel free to submit issues and pull requests.