npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

specrec-ts

v1.0.1

Published

Turn untestable legacy code into comprehensive test suites in minutes - TypeScript implementation of SpecRec

Readme

SpecRec for TypeScript

Turn untestable legacy code into comprehensive test suites in minutes

Introduction: From Legacy Code to Tests in 3 Steps

SpecRec helps you test legacy code by recording real method calls and replaying them as test doubles. Here's the complete workflow:

Step 1: Break Dependencies with create()

Replace direct instantiation (new) with create() to make dependencies controllable:

// Before: Hard dependency
const emailService = new EmailService(connectionString);

// After: Testable dependency
import { create } from 'specrec-ts';
const emailService = create(EmailService)(connectionString);

Step 2: Write a Test (Coming Soon)

Note: Context API and automatic recording/verification are planned features. Currently, you can use manual test doubles with ObjectFactory.

Step 3: Run Test and Fill Return Values (Coming Soon)

Note: Automatic specification generation and Parrot replay are planned features.

Installation

Add to your test project:

npm install specrec-ts

Or with yarn:

yarn add specrec-ts

Core Components

ObjectFactory: Making Dependencies Testable

Use Case: Your legacy code creates dependencies with new, making it impossible to inject test doubles.

Solution: Replace new with create() to enable dependency injection without major refactoring.

In Regular Tests

import { ObjectFactory } from 'specrec-ts';

describe('MyService', () => {
  let factory: ObjectFactory;

  beforeEach(() => {
    factory = new ObjectFactory();
  });

  afterEach(() => {
    factory.clearAll();
  });

  it('should use mock repository', () => {
    // Setup
    const mockRepo = new MockRepository();
    factory.setOne(Repository, mockRepo);

    // Act - your code calls create(Repository)() and gets mockRepo
    const service = new MyService(factory);
    const result = service.processData();

    // Assert
    expect(result).toEqual(expected);
  });
});

Breaking Dependencies

Transform hard dependencies into testable code:

// Legacy code with hard dependency
class UserService {
  processUser(id: number) {
    const repo = new SqlRepository("server=prod;...");
    const user = repo.getUser(id);
    // ...
  }
}

// Testable code using ObjectFactory
import { create } from 'specrec-ts';

class UserService {
  processUser(id: number) {
    const repo = create(SqlRepository)("server=prod;...");
    const user = repo.getUser(id);
    // ...
  }
}

Curried Syntax Benefits

The curried syntax create(Class)(args) provides a clean, functional approach:

// Create a factory function for a specific class
const createEmailService = create(EmailService);

// Use it multiple times with different parameters
const service1 = createEmailService("smtp1.example.com", 587);
const service2 = createEmailService("smtp2.example.com", 465);

// Type inference works perfectly
const repo = create(UserRepository)("connection-string");
// TypeScript knows repo is UserRepository

Test Double Injection

Use Case: You need to replace real services with test doubles during testing.

Solution: Use setOne for single-use mocks or setAlways for persistent test doubles.

Single-Use Test Doubles

import { ObjectFactory } from 'specrec-ts';

const factory = new ObjectFactory();

// Queue a test double for single use
const mockService = new MockEmailService();
factory.setOne(EmailService, mockService);

const service1 = factory.create(EmailService)(); // Returns mockService
const service2 = factory.create(EmailService)(); // Creates new EmailService

Persistent Test Doubles

// Set a persistent test double
const mockDb = new MockDatabase();
factory.setAlways(DatabaseService, mockDb);

const db1 = factory.create(DatabaseService)(); // Returns mockDb
const db2 = factory.create(DatabaseService)(); // Same mockDb instance
const db3 = factory.create(DatabaseService)(); // Still mockDb

Priority: SetOne Over SetAlways

const alwaysMock = new AlwaysMockService();
const onceMock = new OnceMockService();

factory.setAlways(MyService, alwaysMock);
factory.setOne(MyService, onceMock);

const service1 = factory.create(MyService)(); // Returns onceMock
const service2 = factory.create(MyService)(); // Returns alwaysMock

Duck Typing Advantage

Use Case: You have interfaces and multiple implementations but don't want complex registration.

Solution: TypeScript's structural typing automatically handles compatibility.

interface IEmailService {
  send(to: string, subject: string): void;
}

class EmailService implements IEmailService {
  send(to: string, subject: string): void {
    // Real implementation
  }
}

class MockEmailService {
  calls: Array<{to: string; subject: string}> = [];

  send(to: string, subject: string): void {
    this.calls.push({to, subject});
  }
}

// All compatible - no registration needed!
factory.setOne(EmailService, new MockEmailService());
const service: IEmailService = factory.create(EmailService)();
// TypeScript is happy because MockEmailService is structurally compatible

Object ID Tracking

Use Case: Your methods pass around complex objects that are hard to serialize in specifications.

Solution: Register objects with IDs to show clean references instead of verbose dumps.

const factory = new ObjectFactory();

// Complex configuration object
const dbConfig = {
  host: 'localhost',
  port: 5432,
  database: 'testdb',
  credentials: { /* ... */ }
};

// Register with ID
factory.register(dbConfig, 'testDbConfig');

// Later retrieve by ID
const config = factory.getRegistered<DatabaseConfig>('testDbConfig');

// Auto-generate IDs if not provided
const obj1 = { value: 1 };
const id = factory.register(obj1); // Returns 'auto_1'

Constructor Parameter Tracking

Use Case: You need to verify what parameters were passed to constructors during object creation.

Solution: Implement IConstructorCalledWith interface to receive parameter information.

import { IConstructorCalledWith, ConstructorParameterInfo } from 'specrec-ts';

class TrackedService implements IConstructorCalledWith {
  private params?: ConstructorParameterInfo[];

  constructor(
    public config: string,
    public port: number,
    public options?: ServiceOptions
  ) {}

  constructorCalledWith(params: ConstructorParameterInfo[]): void {
    this.params = params;
    // params[0] = { index: 0, type: 'string', value: 'localhost' }
    // params[1] = { index: 1, type: 'number', value: 8080 }
    // params[2] = { index: 2, type: 'object', value: {...} }
  }
}

const service = factory.create(TrackedService)('localhost', 8080, { timeout: 5000 });
// constructorCalledWith is automatically called with parameter details

Clear Operations

Use Case: You need to reset factory state between tests to ensure test isolation.

Solution: Use clear() for specific types or clearAll() for complete reset.

// Clear specific type
factory.clear(EmailService);

// Clear all registrations
factory.clearAll();

// Global singleton cleanup
import { clearAll } from 'specrec-ts';
clearAll(); // Clears the global instance

Global Instance Pattern

Use Case: You want to use ObjectFactory throughout your codebase without passing instances.

Solution: Use the global singleton with convenient exports.

import { create, setOne, setAlways, clearAll } from 'specrec-ts';

// All functions use the global singleton
setAlways(EmailService, mockEmailService);

// Anywhere in your code
const service = create(EmailService)();

// Clean up in test teardown
afterEach(() => {
  clearAll();
});

Advanced Features

Type Safety

TypeScript provides full type safety and IntelliSense:

class UserService {
  constructor(
    private name: string,
    private age: number,
    private admin: boolean
  ) {}
}

// TypeScript enforces correct parameter types
const service = create(UserService)("John", 30, true); // ✅ Correct

// Type errors are caught at compile time
const service2 = create(UserService)(30, "John", true); // ❌ Type error
const service3 = create(UserService)("John"); // ❌ Missing parameters

Working with Async Constructors

While JavaScript doesn't support async constructors directly, you can work with factory patterns:

class AsyncService {
  private constructor(private data: any) {}

  static async create(url: string): Promise<AsyncService> {
    const data = await fetch(url).then(r => r.json());
    return new AsyncService(data);
  }
}

// Use with factory
const servicePromise = AsyncService.create('https://api.example.com');

Migration Examples

Before (Direct Instantiation)

class OrderProcessor {
  processOrder(orderId: string) {
    const db = new DatabaseConnection("prod-server");
    const emailService = new EmailService("smtp.example.com", 587);
    const order = db.getOrder(orderId);

    if (order.status === 'pending') {
      // Process order
      emailService.sendConfirmation(order.customerEmail);
    }
  }
}

After (Using ObjectFactory)

import { create } from 'specrec-ts';

class OrderProcessor {
  processOrder(orderId: string) {
    const db = create(DatabaseConnection)("prod-server");
    const emailService = create(EmailService)("smtp.example.com", 587);
    const order = db.getOrder(orderId);

    if (order.status === 'pending') {
      // Process order
      emailService.sendConfirmation(order.customerEmail);
    }
  }
}

In Tests

import { setOne, clearAll } from 'specrec-ts';

describe('OrderProcessor', () => {
  afterEach(() => clearAll());

  it('should send confirmation email for pending orders', () => {
    // Arrange
    const mockDb = new MockDatabase();
    mockDb.setOrder('123', { status: 'pending', customerEmail: '[email protected]' });

    const mockEmail = new MockEmailService();

    setOne(DatabaseConnection, mockDb);
    setOne(EmailService, mockEmail);

    // Act
    const processor = new OrderProcessor();
    processor.processOrder('123');

    // Assert
    expect(mockEmail.sentEmails).toContainEqual({
      to: '[email protected]',
      type: 'confirmation'
    });
  });
});

Requirements

  • Node.js 14+
  • TypeScript 4.5+
  • Any test framework (Jest, Mocha, Vitest, etc.)

License

See LICENSE.md