@simplicate/sigil
v2.2.1
Published
Sigil - A structured error handling library that acts as a beacon for potential issues in your codebase
Downloads
24
Maintainers
Readme
Sigil (@simplicate/sigil)
A structured error handling library that acts as a beacon for potential issues in your codebase. Sigil provides a flexible and extensible way to process errors in your Node.js/TypeScript applications using the strategy pattern, with built-in support for structured logging and severity-based classification.
What is a Sigil?
A sigil is a personal, symbolic representation of a specific intention or desire, created by condensing an idea into a unique pictographic form. The process involves writing out a goal in words, then removing vowels and repeated letters before combining the remaining letters to form an abstract symbol. The completed sigil is then "charged" with energy through rituals like visualization or meditation and used to manifest the creator's intention.
How does this relate to our error handling library?
Just as a traditional sigil distills a complex intention into a clear, powerful symbol, the Sigil error handling library condenses the complexity of error management into a structured, actionable form. In your codebase, errors can be chaotic and hard to interpret. Sigil acts as a symbolic marker, transforming raw error data into meaningful, structured information that reflects your application's intentions for reliability and clarity.
By defining error handling strategies and structured logging, Sigil helps you "charge" your error handling with purpose: every error is processed, classified, and logged in a way that aligns with your application's goals. This makes error handling not just a reaction, but a deliberate, intention-driven part of your software's design—mirroring the way a sigil is created and used to manifest a specific outcome.
Features
- Strategy Pattern: Easily define custom error handling strategies for different error types
- Application-Level: Framework-agnostic error processing that returns structured error data
- TypeScript Support: Fully typed with TypeScript definitions
- Structured Logging: Built-in support for structured logging with automatic context enrichment
- Severity-Based: Configurable error severity levels (low, medium, high, critical)
- Severity-Aware Logging: Automatic log level selection based on error severity
- Extensible: Easy to extend with custom strategies for domain-specific error handling
Installation
npm install @simplicate/sigilLogging
Sigil provides a comprehensive logging system with built-in support for multiple logging frameworks through a unified AppLogger interface. The logging system features automatic framework detection, graceful fallbacks, and structured logging capabilities.
Quick Start
import { createLogger } from '@simplicate/sigil/logging';
// Auto-detect and create the best available logger
const logger = createLogger({ level: 'info', pretty: true });
// Use structured logging
logger.info({ userId: 123, operation: 'login' }, 'User authentication successful');
logger.error({ error: 'INVALID_TOKEN' }, 'Authentication failed');
// Create child loggers with persistent context
const requestLogger = logger.child({ requestId: 'req-456', service: 'auth' });
requestLogger.info('Processing request'); // Automatically includes requestId & serviceLogger Factory
The createLogger function provides automatic framework detection and configuration:
import { createLogger } from '@simplicate/sigil/logging';
// Auto-detect best available logger (Pino → Winston → Console)
const logger = createLogger({ level: 'info', pretty: true });
// Create specific logger types
const pinoLogger = createLogger('pino', { level: 'debug', pretty: false });
const winstonLogger = createLogger('winston', { level: 'warn' });
const consoleLogger = createLogger('console', { bindings: { service: 'my-app' } });Supported Logger Types
'pino': High-performance JSON logger (requirespinopackage)'winston': Popular Winston logger (requireswinstonpackage)'console': Native console methods (no dependencies)'auto': Auto-detects available framework with graceful fallbacks
Configuration Options
interface LoggerConfig {
level?: 'debug' | 'info' | 'warn' | 'error'; // Log level
pretty?: boolean; // Pretty print (dev mode)
bindings?: Record<string, unknown>; // Default context
}Graceful Fallbacks
The auto-detection system gracefully falls back when dependencies aren't available:
- Pino (preferred) - High performance JSON logging
- Winston (fallback) - If Pino not available
- Console (final fallback) - Always available
// This will never fail - always returns a working logger
const logger = createLogger('auto', { level: 'info' });Using Existing Logger Instances
If you already have logger instances, use adapters to make them compatible:
import { createPinoAdapter, createWinstonAdapter, createConsoleAdapter } from '@simplicate/sigil/logging';
import pino from 'pino';
import winston from 'winston';
// Pino adapter
const pinoInstance = pino({ level: 'debug' });
const pinoLogger = createPinoAdapter(pinoInstance);
// Winston adapter
const winstonInstance = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
const winstonLogger = createWinstonAdapter(winstonInstance);
// Console adapter
const consoleLogger = createConsoleAdapter({ service: 'my-service' });Structured Logging
All loggers support both structured (object + message) and simple (string-only) logging:
// Structured logging (recommended)
logger.error({ userId: 123, operation: 'payment', amount: 99.99 }, 'Payment processing failed');
logger.info({ requestId: 'req-789', duration: 245 }, 'Request completed');
// Simple string logging
logger.info('Application started');
logger.warn('Rate limit approaching');Child Loggers
Create contextual loggers that inherit parent bindings:
const logger = createLogger({ bindings: { service: 'user-service' } });
// Child logger inherits 'service' and adds 'userId'
const userLogger = logger.child({ userId: 123 });
// Child of child - inherits all previous context
const operationLogger = userLogger.child({ operation: 'password-reset' });
operationLogger.info('Starting operation');
// Logs: { service: 'user-service', userId: 123, operation: 'password-reset', msg: 'Starting operation' }Integration with Error Handling
Loggers integrate seamlessly with Sigil's error handling strategies:
import { createLogger } from '@simplicate/sigil/logging';
import { sig } from '@simplicate/sigil';
const logger = createLogger('auto', { level: 'info' });
const errorHandler = new sig.GenericErrorHandler([
new sig.ConfigurableErrorStrategy(ValidationError, {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
severity: 'medium' // Automatically logs at 'warn' level
}),
new sig.ConfigurableErrorStrategy(DatabaseError, {
code: 'DATABASE_ERROR',
message: 'Database operation failed',
severity: 'high' // Automatically logs at 'error' level
})
]);
try {
await riskyOperation();
} catch (error) {
// Error is automatically logged with appropriate level based on severity
const result = errorHandler.handle(error, logger, {
operation: 'user-creation',
userId: 123
});
}Logger Compatibility
The AppLogger interface is compatible with popular logging libraries:
- Pino: Direct compatibility
- Winston: Compatible with structured logging
- Bunyan: Compatible interface
- Custom loggers: Implement the
AppLoggerinterface
No peer dependencies required - use any logger you prefer!
Usage
Basic Usage
import { sig, AppLogger } from '@simplicate/sigil';
import pino from 'pino';
const logger = pino();
// Define custom error classes using sig.AppError base class
class ValidationError extends sig.AppError {
constructor(message: string, public field?: string) {
super('ValidationError', message);
}
}
class DatabaseError extends sig.AppError {
constructor(message: string, public connectionString?: string) {
super('DatabaseError', message);
}
}
// Create error handling strategies
const validationStrategy = new sig.ConfigurableErrorStrategy(ValidationError, {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
severity: 'medium'
});
const databaseStrategy = new sig.ConfigurableErrorStrategy(DatabaseError, {
code: 'DATABASE_ERROR',
message: 'Database operation failed',
severity: 'high'
});
// Create the error handler with strategies
const errorHandler = new sig.GenericErrorHandler([
validationStrategy,
databaseStrategy
]);
// Use in your application
try {
// Some operation that might throw
await processUserData(userData);
} catch (error) {
// Process the error and get structured result
const errorResult = errorHandler.handle(error, logger, {
operation: 'user-processing',
userId: userData.id
});
// Use the structured error result as needed
console.log('Error Code:', errorResult.code);
console.log('Severity:', errorResult.severity);
// In a web framework, you might convert severity to HTTP status:
const httpStatus = severityToHttpStatus(errorResult.severity);
return { status: httpStatus, error: errorResult };
}
// Helper function for web frameworks
function severityToHttpStatus(severity: string): number {
switch (severity) {
case 'low': return 400;
case 'medium': return 422;
case 'high': return 500;
case 'critical': return 503;
default: return 500;
}
}Custom Error Strategy
You can create completely custom error strategies by implementing the ErrorHandlerStrategy interface:
import { ErrorHandlerStrategy, ErrorResult, AppLogger } from '@simplicate/sigil';
class CustomDatabaseErrorStrategy implements ErrorHandlerStrategy {
canHandle(error: unknown): boolean {
// Check if this is a database connection error
return error instanceof Error && error.message.includes('ECONNREFUSED');
}
handle(error: unknown, logger: AppLogger, context: Record<string, unknown>): ErrorResult {
logger.error({ err: error, ...context }, 'Database connection failed');
return {
code: 'DATABASE_CONNECTION_ERROR',
message: 'Database service is temporarily unavailable',
severity: 'critical',
originalError: error,
context
};
}
}
// Usage with custom strategy
const errorHandler = new sig.GenericErrorHandler([
new CustomDatabaseErrorStrategy(),
new sig.DefaultErrorStrategy()
]);API Reference
GenericErrorHandler
The main error handler class that manages error handling strategies.
constructor(strategies: ErrorHandlerStrategy[], fallbackStrategy?: ErrorHandlerStrategy)strategies: Array of error handling strategies, ordered by priorityfallbackStrategy: Optional fallback strategy (defaults to DefaultErrorStrategy)
handle(error: unknown, logger: AppLogger, context?: Record<string, unknown>): ErrorResulterror: The error to handlelogger: Logger instance implementing AppLogger interfacecontext: Optional context object for logging- Returns:
ErrorResult- Structured error data
ConfigurableErrorStrategy
A configurable strategy for handling specific error types.
constructor(errorType: new (...args: any[]) => TError, config: ErrorConfig)errorType: The error class constructorconfig: Error configuration object
DefaultErrorStrategy
A fallback strategy that handles any unmatched errors.
constructor(defaultMessage?: string, defaultCode?: string, defaultSeverity?: SeverityLevel)defaultMessage: Message for unknown errors (default: "An unexpected error occurred.")defaultCode: Error code for unknown errors (default: "UNKNOWN_ERROR")defaultSeverity: Severity level for unknown errors (default: "high")
AppError vs GenericAppError
Sigil provides two error classes to cover different use cases:
AppError (Base Class)
A base class for creating custom application errors with proper stack trace handling.
constructor(name: string, message: string)name: The error name (typically the class name)message: The error message
toJSON(): Record<string, unknown>- Returns: JSON serializable representation of the error
Benefits of using AppError:
- Automatic stack trace optimization using
Error.captureStackTracewhen available - Consistent error name handling
- Built-in JSON serialization support
- Reduces boilerplate code in custom error classes
When to use AppError:
- Creating domain-specific error classes (ValidationError, DatabaseError, etc.)
- You need custom properties and behavior
- Building a structured error hierarchy
- You want full control over error naming and structure
Example:
import { sig } from '@simplicate/sigil';
// Custom error extending sig.AppError
class ValidationError extends sig.AppError {
constructor(message: string, public field?: string) {
super('ValidationError', message);
}
}
class DatabaseError extends sig.AppError {
constructor(message: string, public query?: string, public connectionString?: string) {
super('DatabaseError', message);
}
}
// Usage
const error = new ValidationError('Email format is invalid', 'email');
console.log(error.name); // 'ValidationError'
console.log(error.message); // 'Email format is invalid'
console.log(error.field); // 'email'
// JSON serialization
const serialized = error.toJSON();
console.log(JSON.stringify(serialized));GenericAppError (Concrete Class)
A ready-to-use concrete error class for general error scenarios that don't require custom error classes.
constructor(message?: string, details?: Record<string, unknown>)message: Optional error message (defaults to "An application error occurred")details: Optional object containing additional error context
Features:
- Fixed name: Always has the name
'GenericAppError' - Default message: Uses
'An application error occurred'if no message provided - Details property: Includes an optional
detailsobject for additional context - Extends AppError: Inherits all AppError benefits (stack traces, JSON serialization)
When to use GenericAppError:
- Quick, one-off errors without creating a custom class
- Handling unexpected/catch-all scenarios
- You want to include additional context via the
detailsproperty - Prototyping or when you don't want to create a specific error class yet
- Simple error cases that don't warrant a custom error type
Examples:
import { sig } from '@simplicate/sigil';
// With custom message and details
throw new sig.GenericAppError('Something went wrong during processing', {
userId: 123,
operation: 'delete',
timestamp: new Date().toISOString()
});
// With just a message
throw new sig.GenericAppError('Custom error message');
// With default message
throw new sig.GenericAppError();
// Accessing properties
try {
throw new sig.GenericAppError('Process failed', { step: 'validation', code: 'INVALID_INPUT' });
} catch (error) {
if (error instanceof sig.GenericAppError) {
console.log(error.name); // 'GenericAppError'
console.log(error.message); // 'Process failed'
console.log(error.details); // { step: 'validation', code: 'INVALID_INPUT' }
}
}Choosing Between AppError and GenericAppError:
| Use Case | AppError | GenericAppError | |----------|----------|----------------| | Domain-specific errors | ✅ Recommended | ❌ Not suitable | | Custom properties/methods | ✅ Required | ❌ Limited to details | | Quick prototyping | ❌ More setup | ✅ Recommended | | One-off errors | ❌ Overkill | ✅ Perfect | | Structured error hierarchy | ✅ Essential | ❌ Flat structure | | Additional context needed | ✅ Custom props | ✅ Details object | | Catch-all scenarios | ❌ Too specific | ✅ Ideal |
ErrorConfig
Configuration object for error processing:
type ErrorConfig = {
code: string; // Application-specific error code
message: string; // Human-readable error message
severity: SeverityLevel; // Error severity level
}Structured Logging
The error strategies automatically enrich log entries with structured context including:
- Error codes and types for easy filtering and monitoring
- Severity levels for appropriate log level selection
- Original error objects and user-provided context
// Example structured log output:
// {"level":"warn","err":{...},"errorCode":"VALIDATION_ERROR","errorType":"ValidationError","severity":"medium","userId":123,"msg":"Input validation failed"}ErrorResult
Structured result returned by error processing:
interface ErrorResult {
code: string; // Application-specific error code
message: string; // Human-readable error message
severity: SeverityLevel; // Error severity level
originalError: unknown; // The original error that was processed
context?: Record<string, unknown>; // Additional context or metadata
}SeverityLevel
Error severity levels:
type SeverityLevel = 'low' | 'medium' | 'high' | 'critical';Testing Utilities
Sigil provides comprehensive testing utilities that make writing maintainable, data-driven tests easier. The utilities are framework-agnostic and work with Jest, Vitest, Mocha, and other testing frameworks.
import { createErrorConfig, createTestContext, runTableTests, Utils } from '@simplicate/sigil/testing';Key Features
- 🎯 Table-Driven Tests: Write data-driven tests with minimal boilerplate
- 🏭 Test Data Factories: Consistent, reusable test data creation
- 🔄 Framework Agnostic: Works with any testing framework (Jest, Vitest, Mocha, etc.)
- 📝 TypeScript First: Full type safety and IntelliSense support
- 🧹 DRY Principle: Reduce code duplication and improve maintainability
Quick Comparison: Testing Utilities vs Direct Framework Usage
❌ Traditional Jest/Vitest Approach (Verbose & Repetitive)
describe('Email Validation', () => {
it('should accept valid email addresses', () => {
expect(validateEmail('[email protected]')).toBe(true);
});
it('should reject email without @', () => {
expect(validateEmail('userexample.com')).toBe(false);
});
it('should reject email without domain', () => {
expect(validateEmail('user@')).toBe(false);
});
it('should reject empty email', () => {
expect(validateEmail('')).toBe(false);
});
// ... 20 more similar tests with repetitive structure
});Problems:
- 🚫 Repetitive boilerplate code
- 🚫 Hard to maintain when logic changes
- 🚫 Test data scattered throughout
- 🚫 Verbose for simple scenarios
✅ With Sigil Testing Utilities (Clean & Maintainable)
import { runTableTests } from '@simplicate/sigil/testing';
describe('Email Validation', () => {
interface EmailTestCase {
name: string;
email: string;
expected: boolean;
}
const emailTestCases: EmailTestCase[] = [
{ name: 'should accept valid email addresses', email: '[email protected]', expected: true },
{ name: 'should reject email without @', email: 'userexample.com', expected: false },
{ name: 'should reject email without domain', email: 'user@', expected: false },
{ name: 'should reject empty email', email: '', expected: false },
// ... 20 more cases - just data, no duplication
];
runTableTests(emailTestCases, ({ name, email, expected }) => {
it(name, () => {
expect(validateEmail(email)).toBe(expected);
});
});
});Benefits:
- ✅ DRY: Single test function, multiple data cases
- ✅ Maintainable: Easy to add/modify test cases
- ✅ Data-driven: Clear separation of test data and logic
- ✅ Scalable: Add more test cases without code changes
Testing Utility Functions
1. createErrorConfig() - Error Configuration Factory
Creates consistent error configurations with sensible defaults.
import { createErrorConfig } from '@simplicate/sigil/testing';
// Basic usage - uses defaults
const basicConfig = createErrorConfig();
// Result: { code: 'TEST_ERROR', message: 'Test error message', severity: 'medium' }
// With overrides
const customConfig = createErrorConfig({
code: 'VALIDATION_ERROR',
severity: 'high'
});
// Result: { code: 'VALIDATION_ERROR', message: 'Test error message', severity: 'high' }Use Cases:
- Testing error handler strategies
- Consistent error configurations across tests
- Mock error data creation
2. createTestContext() - Context Factory
Creates consistent context objects with standard test constants.
import { createTestContext } from '@simplicate/sigil/testing';
// Basic usage - uses test constants
const basicContext = createTestContext();
// Result: { userId: 12345, requestId: 'req-test-123', operation: 'test-operation', timestamp: Date }
// With overrides
const customContext = createTestContext({
userId: 999,
operation: 'user-registration',
customField: 'test-value'
});
// Result: { userId: 999, requestId: 'req-test-123', operation: 'user-registration', timestamp: Date, customField: 'test-value' }Use Cases:
- API request context simulation
- Error handler context testing
- Consistent logging context
3. runTableTests() - Table-Driven Testing
Executes the same test logic across multiple data scenarios.
import { runTableTests } from '@simplicate/sigil/testing';
interface TestCase {
name: string;
input: any;
expected: any;
}
const testCases: TestCase[] = [
{ name: 'scenario 1', input: 'data1', expected: 'result1' },
{ name: 'scenario 2', input: 'data2', expected: 'result2' },
// ... more scenarios
];
runTableTests(testCases, ({ name, input, expected }) => {
it(name, () => {
const result = processData(input);
expect(result).toBe(expected);
});
});Use Cases:
- Input validation testing
- Error scenario testing
- API response testing
- Any scenario with multiple similar test cases
4. Utils - Organized Namespace
Access all utilities through a organized namespace.
import { Utils } from '@simplicate/sigil/testing';
const errorConfig = Utils.createErrorConfig({ code: 'CUSTOM_ERROR' });
const context = Utils.createTestContext({ operation: 'test' });
Utils.runTableTests(testCases, testFn);Real-World Examples
Error Handler Strategy Testing
import { createErrorConfig, createTestContext, runTableTests } from '@simplicate/sigil/testing';
import { sig } from '@simplicate/sigil';
describe('Error Handler Strategies', () => {
interface ErrorStrategyCase {
name: string;
error: Error;
expectedCode: string;
expectedSeverity: 'low' | 'medium' | 'high' | 'critical';
}
const strategyCases: ErrorStrategyCase[] = [
{
name: 'should handle validation errors',
error: new ValidationError('Invalid email'),
expectedCode: 'VALIDATION_ERROR',
expectedSeverity: 'medium'
},
{
name: 'should handle database errors',
error: new DatabaseError('Connection failed'),
expectedCode: 'DATABASE_ERROR',
expectedSeverity: 'high'
},
{
name: 'should handle unknown errors',
error: new Error('Unknown error'),
expectedCode: 'UNKNOWN_ERROR',
expectedSeverity: 'high'
}
];
runTableTests(strategyCases, ({ name, error, expectedCode, expectedSeverity }) => {
it(name, () => {
const validationStrategy = new sig.ConfigurableErrorStrategy(ValidationError,
createErrorConfig({ code: 'VALIDATION_ERROR', severity: 'medium' })
);
const dbStrategy = new sig.ConfigurableErrorStrategy(DatabaseError,
createErrorConfig({ code: 'DATABASE_ERROR', severity: 'high' })
);
const errorHandler = new sig.GenericErrorHandler([validationStrategy, dbStrategy]);
const context = createTestContext({ operation: 'strategy-test' });
const mockLogger = jest.fn() as any;
const result = errorHandler.handle(error, mockLogger, context);
expect(result.code).toBe(expectedCode);
expect(result.severity).toBe(expectedSeverity);
});
});
});API Integration Testing
import { createTestContext, runTableTests } from '@simplicate/sigil/testing';
describe('API Error Handling', () => {
interface APITestCase {
name: string;
statusCode: number;
responseBody: any;
expectedErrorCode: string;
expectedSeverity: string;
}
const apiErrorCases: APITestCase[] = [
{
name: 'should handle 400 Bad Request',
statusCode: 400,
responseBody: { error: 'Invalid input' },
expectedErrorCode: 'VALIDATION_ERROR',
expectedSeverity: 'medium'
},
{
name: 'should handle 401 Unauthorized',
statusCode: 401,
responseBody: { error: 'Token expired' },
expectedErrorCode: 'AUTH_ERROR',
expectedSeverity: 'medium'
},
{
name: 'should handle 500 Internal Server Error',
statusCode: 500,
responseBody: { error: 'Database connection failed' },
expectedErrorCode: 'SERVER_ERROR',
expectedSeverity: 'high'
}
];
runTableTests(apiErrorCases, ({ name, statusCode, responseBody, expectedErrorCode, expectedSeverity }) => {
it(name, async () => {
const context = createTestContext({
operation: 'api-request',
endpoint: '/api/users'
});
const mockResponse = {
status: statusCode,
data: responseBody
};
const result = await apiErrorHandler.handleResponse(mockResponse, context);
expect(result.code).toBe(expectedErrorCode);
expect(result.severity).toBe(expectedSeverity);
expect(result.context).toMatchObject(context);
});
});
});Hybrid Approach: Best of Both Worlds
The most effective testing strategy combines Sigil utilities with framework-specific features:
import { createErrorConfig, createTestContext, runTableTests } from '@simplicate/sigil/testing';
import { sig } from '@simplicate/sigil';
describe('Payment Processing System', () => {
// ✅ Use utilities for data-driven scenarios
const paymentValidationCases = [
{ name: 'should accept valid amounts', amount: 100, valid: true },
{ name: 'should reject negative amounts', amount: -50, valid: false },
{ name: 'should reject zero amounts', amount: 0, valid: false },
{ name: 'should reject amounts over limit', amount: 100000, valid: false }
];
runTableTests(paymentValidationCases, ({ name, amount, valid }) => {
it(name, () => {
expect(PaymentValidator.isValid(amount)).toBe(valid);
});
});
// ✅ Use direct Jest/Vitest for complex scenarios
it('should handle Stripe webhook integration with proper mocking', async () => {
// Complex setup that benefits from framework-specific features
const mockStripeWebhook = jest.fn().mockResolvedValue({
id: 'evt_123',
type: 'payment_intent.succeeded',
data: { object: { amount: 2000 } }
});
const context = createTestContext({
operation: 'webhook-processing',
source: 'stripe'
});
// Use Jest-specific mocking capabilities
jest.spyOn(StripeClient.prototype, 'webhooks').mockImplementation(() => ({
constructEvent: mockStripeWebhook
}));
const webhook = new StripeWebhookHandler();
webhook.onPaymentSuccess = jest.fn();
await webhook.process(mockWebhookEvent, context);
expect(webhook.onPaymentSuccess).toHaveBeenCalledWith(
expect.objectContaining({ amount: 2000 })
);
expect(mockStripeWebhook).toHaveBeenCalledTimes(1);
});
// ✅ Use utilities for error scenario testing
const errorScenarios = [
{
name: 'should handle payment declined',
error: new PaymentDeclinedError('Card declined'),
expectedCode: 'PAYMENT_DECLINED',
expectedSeverity: 'medium' as const
},
{
name: 'should handle network timeout',
error: new NetworkTimeoutError('Request timeout'),
expectedCode: 'NETWORK_ERROR',
expectedSeverity: 'high' as const
}
];
runTableTests(errorScenarios, ({ name, error, expectedCode, expectedSeverity }) => {
it(name, () => {
const errorConfig = createErrorConfig({ code: expectedCode, severity: expectedSeverity });
const context = createTestContext({ operation: 'payment-processing' });
const result = paymentErrorHandler.handle(error, mockLogger, context);
expect(result.code).toBe(expectedCode);
expect(result.severity).toBe(expectedSeverity);
});
});
});When to Use Each Approach
Use Sigil Testing Utilities When:
- ✅ Table-driven tests with multiple similar scenarios
- ✅ Consistent test data needed across tests
- ✅ Framework independence desired for portability
- ✅ Large test suites that need maintainability
- ✅ Team consistency is important
- ✅ Data-driven testing patterns
Use Jest/Vitest Direct When:
- ✅ Complex mocking requirements (spies, stubs, module mocks)
- ✅ Framework-specific features needed (snapshots, timers)
- ✅ One-off tests with unique logic
- ✅ Integration with framework ecosystems
- ✅ Learning/prototyping where direct API is clearer
Use Hybrid Approach When:
- ✅ Real-world applications (most common scenario)
- ✅ Team has mixed experience levels
- ✅ Balancing maintainability with flexibility
- ✅ Different test types in the same suite
Framework Compatibility
The testing utilities work seamlessly across testing frameworks:
// Jest
runTableTests(testCases, ({ name, input, expected }) => {
it(name, () => {
expect(processor(input)).toBe(expected);
});
});
// Vitest
runTableTests(testCases, ({ name, input, expected }) => {
test(name, () => {
expect(processor(input)).toBe(expected);
});
});
// Mocha + Chai
runTableTests(testCases, ({ name, input, expected }) => {
it(name, () => {
assert.equal(processor(input), expected);
});
});Migration Benefits
Migrating from direct framework usage to Sigil utilities provides:
- 📊 60-80% reduction in test code volume
- 🎯 Improved maintainability - changes in one place
- 🔄 Framework flexibility - easy to switch testing frameworks
- 📝 Better test documentation - data tables are self-documenting
- 🧹 DRY compliance - no more repetitive test functions
- ⚡ Faster test writing - focus on data, not boilerplate
The utilities complement rather than replace Jest/Vitest, giving you the best of both worlds for comprehensive testing strategies.
Project Structure
The library is organized with a clean, flat structure for easy navigation and maintenance:
src/
├── index.ts # Main barrel export
├── app-error.ts # AppError base class
├── error-handler.ts # GenericErrorHandler class
├── error-types.ts # Types and interfaces
├── logger.ts # Logger interface and types
├── strategies/ # Error handling strategies
│ ├── index.ts
│ ├── configurable-strategy.ts
│ └── default-strategy.ts
└── testing/ # Testing utilities (separate export)
├── index.ts
├── error-test-utils.ts
├── types.ts
└── adapters/ # Test framework adapters
├── adapter-factory.ts
├── jest-adapter.ts
└── vitest-adapter.tsDesign Principles
- Flat Structure: Core files at the root level for easy access
- Logical Grouping: Related functionality properly namespaced (strategies, testing)
- Path Mappings: TypeScript path mappings for clean imports (
@strategies/*,@testing/*) - Separate Exports: Testing utilities available as
@simplicate/sigil/testing - Framework Agnostic: No assumptions about your application framework or testing setup
Logger Interface
The package expects a logger that implements the AppLogger interface:
interface AppLogger {
child(bindings: Record<string, unknown>): AppLogger;
debug(obj: Record<string, unknown>, message?: string): void;
debug(message: string): void;
error(obj: Record<string, unknown>, message?: string): void;
error(message: string): void;
info(obj: Record<string, unknown>, message?: string): void;
info(message: string): void;
warn(obj: Record<string, unknown>, message?: string): void;
warn(message: string): void;
}Compatible with popular loggers:
- Pino: Works directly
- Winston: Works with structured logging format
- Bunyan: Compatible interface
- Custom: Implement the interface for any logger
Example with different loggers:
// Pino (direct compatibility)
import pino from 'pino';
const logger = pino();
// Winston (with adapter if needed)
import winston from 'winston';
const winstonLogger = winston.createLogger({ /* config */ });
// Use directly or create simple adapter
// Custom logger
const customLogger: AppLogger = {
child: (bindings) => ({ ...customLogger, bindings }),
debug: (obj, msg) => console.debug(msg, obj),
error: (obj, msg) => console.error(msg, obj),
info: (obj, msg) => console.info(msg, obj),
warn: (obj, msg) => console.warn(msg, obj),
};License
ISC
