Skip to main content
Home

Test framework

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
2 days ago (0.0.22)

Jobless Machine Framework

A modern TypeScript testing framework with a fluent API for building complex test scenarios with context management and assertions. Designed for comprehensive integration testing with support for HTTP requests, database operations, and custom workflows.

Features

  • 🔄 Fluent API: Chain steps and assertions naturally with method chaining
  • 📋 Context Schema: Type-safe shared state with schema builder and prefix utilities
  • 🌐 HTTP Testing: Built-in HTTP request steps and assertions for API testing
  • 🗄️ Database Integration: Custom repository steps for database operations
  • 🔧 Extensible: Create custom steps, assertions, and value wrappers
  • Async Support: Full promise-based execution with parallel step support
  • 🎯 Type Safe: Complete TypeScript support with generic context typing and schema validation
  • 🚀 Bun Native: Optimized for Bun runtime with fast execution
  • 📊 Value Wrappers: Flexible value abstraction (static, context refs, computed)
  • 🔄 Copy Strategies: Flexible data copying between steps with AllCopy and CompositeCopy
  • 🔍 Value Extraction: VExtractor for extracting values from wrappers

Quick Start

Installation

# Install from JSR
bun add @alator21/jobless-machine

TypeScript Configuration

For full type safety, ensure your tsconfig.json includes these settings:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

Why these settings matter:

  • strict: true - Enables all strict type checking (usually already enabled)
  • noUncheckedIndexedAccess: true - Critical for this framework - ensures proper type narrowing for indexed access

Without noUncheckedIndexedAccess, TypeScript won't catch certain type mismatches when using the schema validation features. See tsconfig.recommended.json in the package for complete recommended settings.

Basic Usage

import {
  createTypedSchema,
  type Prefixed,
} from "./src/context/ContextSchema.ts";
import { PrintStep } from "./src/steps/common/PrintStep.ts";

// Define field types for reuse
type UserFields = {
  id: string;
  name: string;
};

// Create schema with prefixed context types
const schema = createTypedSchema<Prefixed<"user.", UserFields>>();

// Create and execute runner
await schema
  .createRunner()
  .addStep(PrintStep.create("Starting test..."))
  .addStep(CreateUserStep.create(schema.allCopy("user.")))
  .addStep(PrintStep.create("Test completed"))
  .execute();

Context Inspection and Debugging

Monitor context state after each step for debugging:

import {
  createTypedSchema,
  type Prefixed,
} from "./src/context/ContextSchema.ts";
import { PrintStep } from "./src/steps/common/PrintStep.ts";

type UserFields = {
  id: string;
  name: string;
};

const schema = createTypedSchema<Prefixed<"user.", UserFields>>();

// Built-in debug inspector
await schema
  .createRunner()
  .withDebugInspector()
  .addStep(PrintStep.create("Creating user..."))
  .addStep(CreateUserStep.create(schema.allCopy("user.")))
  .assertThat(UserExistsAssertion.create("user."))
  .execute();

// Custom inspector with your logic
const customInspector = (context, stepOrAssertion, stepIndex) => {
  console.log(`Step ${stepIndex + 1} completed: ${stepOrAssertion.type()}`);
  const userIds = context.get("user.id");
  console.log(`Current user IDs in context: ${userIds.length}`);
};

await schema
  .createRunner()
  .withInspector(customInspector)
  .addStep(/* your steps */)
  .execute();

HTTP API Testing Example

import {
  createTypedSchema,
  type Prefixed,
} from "./src/context/ContextSchema.ts";
import { SimpleValueWrapper } from "./src/value/SimpleValueWrapper.ts";
import { HttpAssertion } from "./src/assertion/http/HttpAssertion.ts";

type UserFields = {
  id: string;
  name: string;
  email: string;
};

const userPrefix = "user.";
const schema = createTypedSchema<Prefixed<typeof userPrefix, UserFields>>();

await schema
  .createRunner()
  .addStep(
    CreateUserHttpStep.create(schema.allCopy(userPrefix))
      .withName(SimpleValueWrapper.value("John Doe"))
      .withEmail(SimpleValueWrapper.value("john@example.com")),
  )
  .assertThat(HttpAssertion.success(userPrefix))
  .addStep(
    UpdateUserHttpStep.create(
      schema.allCopy(userPrefix),
      schema.value(`${userPrefix}id`),
    ).withName(SimpleValueWrapper.value("John Smith")),
  )
  .assertThat(HttpAssertion.success(userPrefix))
  .execute();

Core Concepts

Context Schema

The recommended way to create typed contexts with schema utilities:

  • createTypedSchema<T>() - Creates a schema with type-safe helpers
  • schema.createRunner() - Creates a runner with built-in extractor
  • schema.value(key, index?) - Creates type-safe context value wrapper
  • schema.allCopy(prefix) - Creates type-validated copy strategy
  • schema.assertValue(key, callback) - Creates type-safe assertion
  • schema.assertEqual(key1, key2) - Asserts two values are equal
  • schema.assertNotEqual(key1, key2) - Asserts two values are not equal
  • schema.context - Access underlying context
  • schema.extractor - Access VExtractor instance

Prefixed Type Utility

Helper type for creating prefixed field types:

type Prefixed<P extends string, T extends Record<string, unknown>>

Transforms { id: string }{ "prefix.id": string }

Runner

The main orchestrator that manages test execution flow:

  • Runner.createWithContext<T>(context, extractor) - Creates runner with context and extractor
  • addStep(step) - Adds a step to the execution timeline
  • addStepTimes(step, times) - Adds the same step multiple times
  • assertThat(assertion) - Adds an assertion to the timeline
  • withInspector(inspector) - Adds custom context inspection after each step/assertion
  • withDebugInspector() - Adds built-in debug logging of context state
  • execute() - Runs all steps and assertions sequentially

Context

Type-safe key-value store for sharing data between steps:

  • add<K>(key, value) - Store a value wrapper
  • get<K>(key) - Retrieve all values for a key
  • getSpecific<K>(key, index) - Get value at specific index
  • getRandom<K>(key) - Get a random value
  • getRandomAndRemove<K>(key) - Get and remove a random value

Value Wrapper System

Flexible value abstraction for deferred resolution:

  • SimpleValueWrapper - Wraps static values
  • ContextValueWrapper - References values stored in context
  • ProviderValueWrapper - Lazily computed values
  • DummyValueWrapper - Placeholder values

VExtractor

Extracts actual values from value wrappers:

  • extract<Val>(wrapper) - Extracts value from any wrapper type
  • extractSpecificFromContext<Val, K>(key) - Extracts specific context value
  • Handles SimpleValue, ContextValue, and ProviderValue wrappers

Steps

Actions that can be executed in sequence:

  • PrintStep - Logs messages to console
  • WaitStep - Waits for specified duration
  • HttpRequestStep - Makes HTTP requests
  • CompositeParallelStep - Executes multiple steps in parallel
  • Custom steps by extending Step<T> class

Assertions

Validations that run against context and external systems:

  • ContextAssertion - Validates context values
  • HttpAssertion - Validates HTTP response status and data
  • Custom assertions by extending Assertion<T> class

Copy Strategies

Control how data flows between steps:

  • AllCopy - Copies all step outputs to context with prefix
  • CompositeCopy - Combines multiple copy strategies
  • SimpleCopy - Basic copying functionality

Project Structure

src/
├── runner/           # Main test execution engine
├── context/          # Type-safe context management
│   ├── Context.ts    # Core context implementation
│   └── ContextSchema.ts  # Schema builder with utilities
├── steps/            # Built-in and custom step implementations
│   ├── common/       # Common steps (Print, Wait)
│   └── http/         # HTTP request steps
├── assertion/        # Assertion framework
│   └── http/         # HTTP-specific assertions
├── value/            # Value wrapper system
│   ├── ValueWrapper.ts        # Base interface
│   ├── SimpleValueWrapper.ts  # Static values
│   ├── ContextValueWrapper.ts # Context references
│   └── ProviderValueWrapper.ts # Computed values
├── copy/             # Data copying strategies
└── extractors/       # Value extraction utilities
    └── VExtractor.ts # Extract values from wrappers

Examples

The framework includes a complete Pet Shop API example demonstrating:

  • RESTful API testing with HTTP steps
  • Database repository integration
  • Complex entity validation
  • Multi-step workflows with context sharing

Run the example:

bun run dev  # Starts the pet shop API server
bun test examples/pet-shop-api/tests/owner.test.ts

Development

Running Tests

bun test              # Run all tests
bun test --watch      # Run tests in watch mode

Type Checking

bun run typecheck     # Type check without compilation

Building

bun run build         # Build for distribution

Formatting

bun run format        # Format code with Prettier

Creating Custom Steps

import { Step } from "./src/steps/Step";
import { Copy } from "./src/copy/Copy";

class MyCustomStep<T extends Record<string, unknown>> extends Step<T> {
  constructor(copy: Copy<T>) {
    super(copy);
  }

  static create<T extends Record<string, unknown>>(
    copy: Copy<T>,
  ): MyCustomStep<T> {
    return new MyCustomStep(copy);
  }

  type(): string {
    return "MyCustomStep";
  }

  async run(): Promise<void> {
    await super.run();
    // Your custom logic here
    console.log("Executing custom step logic");
  }

  protected ignoredProperties(): string[] {
    return []; // Properties to exclude from context copying
  }
}

Creating Custom Assertions

import { Assertion } from "./src/assertion/Assertion";
import { VExtractor } from "./src/extractors/VExtractor";

class MyCustomAssertion<
  T extends Record<string, unknown>,
> extends Assertion<T> {
  constructor() {
    super();
  }

  static create<T extends Record<string, unknown>>(): MyCustomAssertion<T> {
    return new MyCustomAssertion<T>();
  }

  type(): string {
    return "MyCustomAssertion";
  }

  async validate(extractor: VExtractor<T>): Promise<void> {
    await super.validate(extractor);
    // Your validation logic here
    const value = extractor.extractSpecificFromContext("someKey");
    if (!value) {
      throw new Error("Expected value not found in context");
    }
  }
}

Publishing

This package is published to JSR (JavaScript Registry).

For TypeScript configuration recommendations, see the "TypeScript Configuration" section above or check tsconfig.recommended.json included in the package.

License

ISC

New Ticket: Report package

Please provide a reason for reporting this package. We will review your report and take appropriate action.

Please review the JSR usage policy before submitting a report.

Add Package

deno add jsr:@alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";
or

Import directly with a jsr specifier

import * as jobless_machine from "jsr:@alator21/jobless-machine";

Add Package

pnpm i jsr:@alator21/jobless-machine
or (using pnpm 10.8 or older)
pnpm dlx jsr add @alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";

Add Package

yarn add jsr:@alator21/jobless-machine
or (using Yarn 4.8 or older)
yarn dlx jsr add @alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";

Add Package

vlt install jsr:@alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";

Add Package

npx jsr add @alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";

Add Package

bunx jsr add @alator21/jobless-machine

Import symbol

import * as jobless_machine from "@alator21/jobless-machine";