UNPKG

@scintilla-network/litest

Version:

Dependency-free test framework with full Vitest API compatibility. Zero-dependency replacement for Vitest to reduce risk of supply chain attacks.

607 lines (467 loc) 16 kB
# Litest A dependency-free test framework that provides full Vitest API compatibility. Designed as a secure replacement for Vitest to reduce risk of supply chain attacks while maintaining as much feature parity as possible for common use cases. ## Why use Litest? Litest is born upon the realisation that most of the dependencies in Scintilla Network's node_modules were from vitest - this including some ^ dependencies for even stuff like coloring in the terminal seems highly excessive. Hence, Litest was born. ## Features ### Core Testing API - ✅ **Familiar API** - Similar `describe`, `it`, `test`, `suite`, `expect` compatibility - ✅ **Nested test suites** - Support for deeply nested describe blocks - ✅ **Expect matchers** - 15+ expectation matchers including `toEqual`, `toBe`, `toMatch`, `toContain`, `toHaveProperty`, etc. - ✅ **Test aliases** - Support for `test` (alias for `it`) and `suite` (alias for `describe`) ### Test Modifiers & Control Flow - ✅ **Basic modifiers** - `.only`, `.skip`, `.todo`, `.fails` - ✅ **Conditional tests** - `.skipIf()`, `.runIf()` for environment-based testing - ✅ **Parameterized testing** - `.each()` and `.for()` for data-driven tests - ✅ **Test configuration** - Custom timeout, retry, and concurrent options ### Lifecycle & Hooks - ✅ **Lifecycle hooks** - `beforeAll`, `beforeEach`, `afterAll`, `afterEach` - ✅ **Test hooks** - `onTestFinished`, `onTestFailed` for cleanup and debugging - ✅ **Hook inheritance** - hook execution across nested suites ### Miscellaneous Features - ✅ **Error handling** - Professional error reporting with stack traces - ✅ **File execution** - Run individual spec files, directories, or glob patterns - ✅ **Zero dependencies** - Pure JavaScript implementation with built-in utilities, no external dependencies, ES Modules support Note: Some features are not implemented yet, ultra early stage of development. Feel free to contribute. Please open an issue if you find a bug or have a feature request. ## Installation ```bash npm install --save-dev @scintilla-network/litest ``` Or for global installation (I would not recommend this): ```bash npm install -g @scintilla-network/litest ``` ## Usage ### Basic Test Structure ```javascript import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach } from '@scintilla-network/litest'; describe('My Test Suite', () => { beforeAll(() => { // Setup before all tests in this suite console.log('Setting up test suite'); }); beforeEach(() => { // Setup before each test console.log('Setting up individual test'); }); afterEach(() => { // Cleanup after each test console.log('Cleaning up after test'); }); afterAll(() => { // Cleanup after all tests in this suite console.log('Tearing down test suite'); }); describe('nested functionality', () => { it('should work correctly', () => { expect(2 + 2).toEqual(4); }); it('should handle edge cases', () => { expect(() => { throw new Error('Input must be valid'); }).toThrow('Input must be'); }); }); }); ``` ### Advanced Test Features #### Test Modifiers and Configuration ```javascript import { describe, it, test, expect } from '@scintilla-network/litest'; describe('Advanced Features', () => { // Test aliases test('using test alias', () => { expect('test alias').toEqual('test alias'); }); // Test modifiers it.todo('implement this feature later'); // Marks test as todo it.fails('this test is expected to fail', () => { throw new Error('Expected failure'); }); // Conditional testing const isCI = process.env.CI === 'true'; it.skipIf(isCI)('skip in CI environment', () => { // Only runs locally }); it.runIf(!isCI)('run only locally', () => { // Only runs when not in CI }); // Test configuration with options it('custom timeout', { timeout: 5000 }, async () => { await new Promise(resolve => setTimeout(resolve, 1000)); }); it('with retry', { retry: 3 }, () => { // Will retry up to 3 times if it fails expect(Math.random()).toBeGreaterThan(0.1); }); // Multiple options it('full configuration', { timeout: 10000, retry: 2 }, async () => { await someAsyncOperation(); }); }); ``` #### Parameterized Testing ```javascript describe('Parameterized Tests', () => { // Using it.each with arrays it.each([ [1, 2, 3], [2, 3, 5], [3, 4, 7] ])('should add %i + %i = %i', (a, b, expected) => { expect(a + b).toBe(expected); }); // Using it.for it.for([ { input: 'hello', expected: 5 }, { input: 'world', expected: 5 } ])('should calculate length of $input', ({ input, expected }) => { expect(input.length).toBe(expected); }); describe.each([ { framework: 'litest', version: '1.0.0' }, { framework: 'vitest', version: '3.x' } ])('Framework: $framework', ({ framework, version }) => { it(`should work with ${framework} v${version}`, () => { expect(framework).toBeTruthy(); expect(version).toBeTruthy(); }); }); }); ``` #### Test Hooks for Cleanup ```javascript import { onTestFinished, onTestFailed } from '@scintilla-network/litest'; describe('Test Hooks', () => { it('should cleanup resources', () => { const resource = createResource(); // Always runs after test completion onTestFinished(() => { resource.cleanup(); }); // Only runs if test fails onTestFailed((result) => { console.log('Test failed:', result.name); debugResource(resource); }); expect(resource.isActive).toBe(true); }); }); ``` #### Expect Matchers ```javascript describe('Matchers', () => { it('should support string matching', () => { expect('hello world').toMatch(/world/); expect('hello world').toMatch('world'); expect('hello world').not.toMatch('goodbye'); }); it('should support array/string contains', () => { expect([1, 2, 3]).toContain(2); expect('hello world').toContain('world'); expect([1, 2, 3]).not.toContain(4); }); it('should support property checking', () => { const obj = { user: { name: 'John', profile: { active: true } } }; expect(obj).toHaveProperty('user.name', 'John'); expect(obj).toHaveProperty('user.profile.active', true); expect(obj).toHaveProperty(['user', 'name'], 'John'); }); it('should support numeric comparisons', () => { expect(10).toBeGreaterThan(5); expect(10).toBeGreaterThanOrEqual(10); expect(5).toBeLessThan(10); expect(5).toBeLessThanOrEqual(5); expect(0.1 + 0.2).toBeCloseTo(0.3, 1); }); it('should support length checking', () => { expect([1, 2, 3]).toHaveLength(3); expect('hello').toHaveLength(5); expect([]).toHaveLength(0); }); }); ``` ### Running Tests Create test files with `.spec.js` extension and run: ```bash # Run all tests in current directory npm test # Run specific test file npm test path/to/test.spec.js # Run tests in specific directory npm test path/to/tests/ # Using global installation litest litest path/to/test.spec.js litest path/to/tests/ # Or directly with node node src/runner.js node src/runner.js path/to/test.spec.js ``` ### Available Matchers #### Equality Matchers - `toEqual(expected)` - Deep equality comparison - `toBe(expected)` - Strict equality (===) #### Truthiness Matchers - `toBeTruthy()` - Checks if value is truthy - `toBeFalsy()` - Checks if value is falsy - `toBeNull()` - Checks if value is null - `toBeUndefined()` - Checks if value is undefined #### Error Matchers - `toThrow()` - Checks if function throws any error - `toThrow(message)` - Checks if function throws error containing message - `toThrow(/pattern/)` - Checks if function throws error matching regex #### Negation All matchers support negation with `.not`: ```javascript expect(value).not.toEqual(otherValue); expect(fn).not.toThrow(); ``` ### Lifecycle Hooks Litest supports four lifecycle hooks that allow you to run setup and teardown code: #### beforeAll Runs once before all tests in a describe block: ```javascript describe('Database Tests', () => { beforeAll(() => { // Connect to database database.connect(); }); // ... tests }); ``` #### beforeEach Runs before each individual test: ```javascript describe('User Tests', () => { beforeEach(() => { // Reset user state before each test currentUser = null; }); // ... tests }); ``` #### afterEach Runs after each individual test: ```javascript describe('API Tests', () => { afterEach(() => { // Clean up API calls mockServer.reset(); }); // ... tests }); ``` #### afterAll Runs once after all tests in a describe block: ```javascript describe('Integration Tests', () => { afterAll(() => { // Disconnect from services database.disconnect(); }); // ... tests }); ``` #### Hook Inheritance Hooks are inherited by nested describe blocks: ```javascript describe('Parent Suite', () => { beforeEach(() => { console.log('Parent beforeEach'); }); describe('Child Suite', () => { beforeEach(() => { console.log('Child beforeEach'); }); it('test', () => { // Both parent and child beforeEach will run // Output: "Parent beforeEach", "Child beforeEach" }); }); }); ``` #### Hook Execution Order - `beforeAll` hooks run from outermost to innermost - `beforeEach` hooks run from outermost to innermost - `afterEach` hooks run from innermost to outermost - `afterAll` hooks run from innermost to outermost ### Test Timeouts Litest includes built-in timeout support to prevent tests from hanging indefinitely. #### Default Timeout Tests have a default timeout of 5 seconds (5000ms). #### Setting Custom Timeouts Use `setTestTimeout()` to configure timeout for tests in the current suite: ```javascript import { describe, it, expect, setTestTimeout } from '@scintilla-network/litest'; describe('API Tests', () => { // Set timeout to 10 seconds for slow API calls setTestTimeout(10000); it('should handle slow API response', async () => { const response = await slowApiCall(); expect(response).toBeTruthy(); }); }); describe('Fast Unit Tests', () => { // Set shorter timeout for unit tests setTestTimeout(1000); it('should calculate quickly', () => { expect(2 + 2).toBe(4); }); }); ``` #### Timeout Behavior - Timeouts apply to the entire test execution including hooks - When a test times out, it fails with a timeout error message - Each test file starts with the default timeout (5000ms) - Timeout settings are scoped to the suite where `setTestTimeout()` is called - Child suites inherit timeout from parent suites ### Test Modifiers #### Skip Tests ```javascript describe.skip('skipped suite', () => { // This entire suite will be skipped }); it.skip('skipped test', () => { // This individual test will be skipped }); ``` #### Only Run Specific Tests ```javascript describe.only('only this suite', () => { // Only this suite will run }); it.only('only this test', () => { // Only this test will run }); ``` ## Example ```javascript import { describe, it, expect } from '@scintilla-network/litest'; describe('Array Operations', () => { describe('push method', () => { it('should add element to end of array', () => { const arr = [1, 2, 3]; arr.push(4); expect(arr).toEqual([1, 2, 3, 4]); expect(arr.length).toBe(4); }); it('should return new length', () => { const arr = ['a', 'b']; const newLength = arr.push('c'); expect(newLength).toBe(3); }); }); describe('error handling', () => { it('should throw when accessing invalid index', () => { expect(() => { const arr = [1, 2, 3]; if (arr[10] === undefined) { throw new Error('Index out of bounds'); } }).toThrow('Index out of bounds'); }); }); }); ``` ## CLI Options The runner automatically finds all `.spec.js` files in your project directory and subdirectories. ```bash # Run tests in current directory litest # Run tests in specific directory litest path/to/tests ``` ## Output Format Litest provides output similar to Vitest: ``` 🚀 Running tests... example.spec.js ✓ Example Test Suite Basic Math should add correctly ✓ Example Test Suite Basic Math should multiply correctly 2ms ↓ Example Test Suite Basic Math skipped test ✗ Example Test Suite Errors should handle failures 5ms Expected 1 to equal 2 at Object.toEqual (file:///path/to/test.js:10:20) Test Files 1 passed (1) Tests 2 passed | 1 skipped (4) Duration 15ms ❌ Some tests failed! ``` ## Migration from Vitest Litest provides API compatibility with Vitest for common use cases. Migration should be as simple as: ### Update Dependencies ```bash # Remove Vitest npm uninstall vitest # Install Litest npm install --save-dev @scintilla-network/litest ``` ### Update Import Statements ```javascript // Before (Vitest) import { describe, it, test, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest'; // After (Litest) - exact same API import { describe, it, test, expect, beforeAll, beforeEach, afterAll, afterEach } from '@scintilla-network/litest'; ``` ### Supported Vitest Features ✅ **Fully Compatible:** - `describe`, `it`, `test`, `suite` with all modifiers (`.skip`, `.only`, `.todo`, `.fails`) - `expect` with 15+ matchers (`toEqual`, `toBe`, `toMatch`, `toContain`, `toHaveProperty`, etc.) - Lifecycle hooks (`beforeAll`, `beforeEach`, `afterAll`, `afterEach`) - Test hooks (`onTestFinished`, `onTestFailed`) - Parameterized testing (`.each()`, `.for()`) - Conditional testing (`.skipIf()`, `.runIf()`) - Test configuration (`timeout`, `retry`) ⚠️ **Not Implemented (Yet):** - `vi` mocking utilities - `expect.extend()` custom matchers - Coverage reporting (placeholder exists) - Watch mode (placeholder exists) - Snapshot testing ### Example Migration ```javascript // This Vitest test file works unchanged with Litest import { describe, it, expect, beforeAll, afterAll } from '@scintilla-network/litest'; // Changed from 'vitest' describe('User Service', () => { let userService; beforeAll(() => { userService = new UserService(); }); afterAll(() => { userService.cleanup(); }); it.each([ { name: 'John', age: 30, valid: true }, { name: 'Jane', age: 25, valid: true }, { name: '', age: 20, valid: false } ])('should validate user: $name', ({ name, age, valid }) => { const user = { name, age }; expect(userService.isValid(user)).toBe(valid); }); it('should handle async operations', async () => { const user = await userService.createUser('Alice', 28); expect(user).toHaveProperty('id'); expect(user.name).toBe('Alice'); }); }); ``` ## Package.json Integration Add to your `package.json`: ```json { "scripts": { "test": "litest" } } ``` ## License MIT ## Contributing This is a simple, focused test framework. Contributions are welcome for bug fixes and small improvements that maintain the simplicity and ease of maintenance. Please open an issue if you find a bug or have a feature request.