Testing JAM Services
This guide covers how to write integration tests for JAM services using the jammin SDK testing utilities.
Overview
The jammin SDK provides a comprehensive testing framework for simulating JAM accumulation in your test environment. The core of this framework is the TestJam class, which provides a fluent API for creating work reports, running accumulation, and inspecting state changes.
Setup
Prerequisites
Before writing tests, ensure your services are built:
jammin build
This generates service binaries and creates config/jammin.test.config.ts with type-safe service mappings for use in tests.
Installation
The testing utilities are included in the @fluffylabs/jammin-sdk package:
bun add -d @fluffylabs/jammin-sdk
Basic Test Structure
Here’s a minimal test using Bun’s built-in test runner with custom assertions:
import { test } from "bun:test";
import {
CoreId,
createWorkReportAsync,
expectAccumulationSuccess,
Gas,
TestJam,
} from "@fluffylabs/jammin-sdk";
import { SERVICES, testJam } from "../config/jammin.test.config";
test("should process work report", async () => {
// Create a work report
const report = await createWorkReportAsync({
results: [{ serviceId: SERVICES.test.id, gas: Gas(1000n) }],
});
// Execute accumulation
const result = await testJam.withWorkReport(report).accumulate();
// Use custom assertion helper
expectAccumulationSuccess(result);
});
TestJam Class
The TestJam class is the main entry point for testing JAM services. It manages state, work reports, and accumulation execution.
Creating a TestJam Instance
With Loaded Services
// Loads all services from your project's jammin.build.yml files
const jam = await TestJam.create();
This is the recommended approach for most tests as it automatically discovers and loads your service configurations.
With Empty State
// Creates an empty state with no services
const jam = TestJam.empty();
Useful for testing edge cases or when you don’t need any services.
Using Generated Service Configuration
After building your project with jammin build, a type-safe configuration file is automatically generated at config/jammin.test.config.ts. This file provides convenient mappings and a pre-configured TestJam instance:
Option 1: Use Pre-configured TestJam Instance
The simplest approach - the generated config includes a ready-to-use TestJam instance:
import { testJam, SERVICES } from "./config/jammin.test.config.js";
test("should process work report for auth service", async () => {
// testJam is already created with all services loaded
const report = await createWorkReportAsync({
results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }],
});
const result = await testJam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result);
});
Option 2: Create Individual TestJam Instances
For test isolation, create a new instance in each test:
import { SERVICES } from "./config/jammin.test.config.js";
test("should process work report for auth service", async () => {
const jam = await TestJam.create();
// Use the type-safe service mapping
const report = await createWorkReportAsync({
results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }],
});
const result = await jam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result);
});
The generated configuration includes:
testJam- Pre-configured TestJam instance with all services loaded (use for simplicity)SERVICES- Type-safe mapping of service names to IDsTEST_CHAIN_SPEC- Pre-configured chain specification
Adding Work Reports
Work reports can be added using the fluent withWorkReport() API:
const report = await createWorkReportAsync({
results: [
{ serviceId: SERVICES.auth.id, gas: Gas(1000n) },
{ serviceId: SERVICES.bank.id, gas: Gas(2000n) },
],
});
const result = await jam.withWorkReport(report).accumulate();
Multiple Work Reports
Chain multiple withWorkReport() calls to process multiple reports in a single accumulation:
const report1 = await createWorkReportAsync({
results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }],
});
const report2 = await createWorkReportAsync({
results: [{ serviceId: SERVICES.bank.id, gas: Gas(2000n) }],
});
const result = await jam
.withWorkReport(report1)
.withWorkReport(report2)
.accumulate();
console.log(`Processed ${result.accumulationStatistics.size} work items`);
Configuring Accumulation Options
Use withOptions() to configure accumulation behavior:
import { PvmBackend } from "@fluffylabs/jammin-sdk/config";
const result = await jam
.withWorkReport(report)
.withOptions({
slot: Slot(100), // Custom time slot
debug: true, // Enable debug logging
pvmBackend: PvmBackend.BuiltIn, // Use built-in PVM
sequential: true, // Sequential accumulation (default)
})
.accumulate();
Available Options
slot?: TimeSlot- Time slot for accumulation (defaults to state’s current timeslot)debug?: boolean | DebugLoggingOptions- Enable debug logging (can be a boolean for all logs, or an object for fine-grained control)pvmBackend?: PvmBackend- PVM backend to use (PvmBackend.AnanasorPvmBackend.BuiltIn)sequential?: boolean- Use sequential accumulation mode (default:true)entropy?: EntropyHash- Entropy for randomness (defaults to zero hash for deterministic tests)chainSpec?: ChainSpec- Chain specification to use
Querying State
After accumulation, you can query the service state:
Get Service Info
const info = jam.getServiceInfo(SERVICES.auth.id);
console.log(`Balance: ${info?.balance}`);
console.log(`Code hash: ${info?.codeHash}`);
Get Service Storage
const key = TestJam.stringToBlob("testKey");
const value = jam.getServiceStorage(SERVICES.auth.id, key);
Get Preimage Data
const preimage = jam.getServicePreimage(SERVICES.auth.id, someHash);
Creating Work Reports
The SDK provides flexible utilities for creating work reports with varying levels of detail.
Simple Work Report
import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk";
const report = await createWorkReportAsync({
results: [
{ serviceId: SERVICES.auth.id, gas: Gas(1000n) },
],
});
Work Report with Multiple Work Items
const report = await createWorkReportAsync({
results: [
{
serviceId: SERVICES.auth.id,
gas: Gas(1000n),
result: { type: "ok", output: TestJam.numbersToBlob([1, 2, 3]) }
},
{
serviceId: SERVICES.auth.id,
gas: Gas(2000n),
result: { type: "ok" }
},
{
serviceId: SERVICES.auth.id,
gas: Gas(500n),
result: { type: "panic" } // Simulate a panic
},
],
});
Work Report with Custom Configuration
import { CoreId, Slot } from "@fluffylabs/jammin-sdk";
const report = await createWorkReportAsync({
coreIndex: CoreId(5),
results: [
{
serviceId: SERVICES.auth.id,
gas: Gas(1000n),
payload: TestJam.numbersToBlob([4, 5, 6]),
},
],
context: {
lookupAnchorSlot: Slot(42),
},
});
Work Result Types
Work results can have different status types:
// Successful execution
{ type: "ok", output: TestJam.hexToBlob("0xaabbccdd") }
// Execution errors
{ type: "panic" }
{ type: "outOfGas" }
{ type: "badCode" }
{ type: "digestTooBig" }
{ type: "incorrectNumberOfExports" }
{ type: "codeOversize" }
Testing Helpers
The SDK provides assertion helpers to simplify test writing.
expectAccumulationSuccess
Assert that accumulation completed with a valid structure:
import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers";
const result = await jam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result); // Throws if result is invalid
expectStateChange
Assert that state changed according to a predicate:
import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers";
const beforeBalance = jam.getServiceInfo(SERVICES.auth.id)?.balance;
await jam.withWorkReport(report).accumulate();
const afterBalance = jam.getServiceInfo(SERVICES.auth.id)?.balance;
expectStateChange(
beforeBalance,
afterBalance,
(before, after) => after !== undefined && before !== undefined && after > before,
"Balance should increase"
);
expectServiceInfoChange
Specialized helper for validating service account changes:
import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers";
const before = jam.getServiceInfo(SERVICES.auth.id);
await jam.withWorkReport(report).accumulate();
const after = jam.getServiceInfo(SERVICES.auth.id);
expectServiceInfoChange(
before,
after,
(b, a) => a && b && a.balance > b.balance,
"Service should consume gas"
);
Common Test Patterns
Testing Service Execution
import { test, expect } from "bun:test";
import {createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk";
import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers";
import { testJam, SERVICES } from "./config/jammin.test.config.js";
test("service should execute successfully", async () => {
const report = await createWorkReportAsync({
results: [{ serviceId: SERVICES.auth.id, gas: Gas(100000n) }],
});
const result = await testJam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result);
});
Testing State Changes
import { test, expect } from "bun:test";
import { createWorkReportAsync, Gas, TestJam } from "@fluffylabs/jammin-sdk";
import { testJam as jam, SERVICES } from "./config/jammin.test.config.js";
test("service storage should update", async () => {
const authId = SERVICES.auth.id;
const storageKey = TestJam.stringToBlob("myKey");
const beforeValue = jam.getServiceStorage(authId, storageKey);
const report = await createWorkReportAsync({
results: [{ serviceId: authId, gas: Gas(50000n) }],
});
await jam.withWorkReport(report).accumulate();
const afterValue = jam.getServiceStorage(authId, storageKey);
expect(afterValue).not.toEqual(beforeValue);
});
Testing Multiple Services
import { test, expect } from "bun:test";
import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk";
import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers";
import { testJam as jam, SERVICES } from "./config/jammin.test.config.js";
test("should process multiple services", async () => {
const report = await createWorkReportAsync({
results: [
{ serviceId: SERVICES.auth.id, gas: Gas(10000n) },
{ serviceId: SERVICES.bank.id, gas: Gas(20000n) },
{ serviceId: SERVICES.exchange.id, gas: Gas(15000n) },
],
});
const result = await jam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result);
});
Testing Error Conditions
import { test, expect } from "bun:test";
import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk";
import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers";
import { testJam as jam, SERVICES } from "./config/jammin.test.config.js";
test("should handle panic gracefully", async () => {
const report = await createWorkReportAsync({
results: [
{
serviceId: ServiceId(0),
gas: Gas(1000n),
result: { type: "panic" }
},
],
});
const result = await jam.withWorkReport(report).accumulate();
expectAccumulationSuccess(result);
});
Troubleshooting
Service Not Found
If you see errors like “Service with id X not found”, ensure that:
- Your
jammin.build.ymlfile is properly configured - You’ve run
jammin buildcommand - You’re using
SERVICESfrom generatedconfig/jammin.test.config.js;
Type Errors with Branded Types
The SDK uses branded types for safety. Use the helper functions to create them:
import { ServiceId, Gas, CoreId, Slot, U32 } from "@fluffylabs/jammin-sdk";
// Correct
const serviceId = ServiceId(0);
const gas = Gas(1000n);
const coreId = CoreId(5);
const slot = Slot(100);
const value = U32(42);
// Incorrect - will cause type errors
const serviceId = 0; // Type error
const gas = 1000n; // Type error
Debug Logging
Enable debug logging to see what’s happening during accumulation:
const result = await jam
.withWorkReport(report)
.withOptions({ debug: true })
.accumulate();
This will output detailed logs including:
- Accumulation steps
- PVM host calls
Fine-Grained Logging Control
You can enable only specific log categories for more focused debugging:
// Enable only ecalli traces
const result = await jam
.withWorkReport(report)
.withOptions({
debug: {
ecalliTrace: true,
},
})
.accumulate();
Available debug options:
pvmExecution- PVM (Polkadot Virtual Machine) execution details including instruction traces and memory access patternsecalliTrace- Ecalli (host call) traces for service execution and debugging service interactionshostCalls- Host calls made during service executionaccumulate- Accumulation process details and state transitions showing how work items are processedsafrole- Safrole (randomness and validator selection) operations for consensus-related debuggingrefine- Refinement process for work reports including validation and processingstateTransitions- State transitions and state root changes during processing
State Not Persisting
Remember that accumulate() automatically applies state updates. If you need to inspect state at different points:
// Check initial state
const initialInfo = jam.getServiceInfo(SERVICES.auth.id);
// Run first accumulation (state is updated)
await jam.withWorkReport(report1).accumulate();
// Check intermediate state
const midInfo = jam.getServiceInfo(SERVICES.auth.id);
// Run second accumulation (state is updated again)
await jam.withWorkReport(report2).accumulate();
// Check final state
const finalInfo = jam.getServiceInfo(SERVICES.auth.id);
Advanced Usage
Custom Blake2b Hasher
For more control over work report creation:
import { Blake2b } from "@fluffylabs/jammin-sdk/hash";
import { createWorkReport } from "@fluffylabs/jammin-sdk";
const blake2b = await Blake2b.createHasher();
const report = createWorkReport(blake2b, {
results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }],
});
Manual State Management
For advanced use cases, you can manually manage state:
import { generateState, loadServices } from "@fluffylabs/jammin-sdk";
const services = await loadServices();
const state = generateState(services);
// Use state directly with simulateAccumulation
import { simulateAccumulation } from "@fluffylabs/jammin-sdk";
const result = await simulateAccumulation(state, [report], {
debug: true,
});
Custom Chain Specifications
Override the default chain spec:
import { tinyChainSpec } from "@fluffylabs/jammin-sdk/config";
const customSpec = {
...tinyChainSpec,
// Customize as needed
};
const result = await jam
.withWorkReport(report)
.withOptions({ chainSpec: customSpec })
.accumulate();
Best Practices
- Use
testJamby default - It automatically loads your services - Use preconfigured
SERVICESinstead of manually writing service ids - It prevents changing tests when reassigning service ids - Chain method calls - The fluent API makes tests more readable
- Use helper assertions - They provide better error messages than raw
expect() - Test state changes explicitly - Don’t assume accumulation modified state
- Use branded types - They prevent common mistakes with raw numbers
- Enable debug logging when troubleshooting - It shows exactly what’s happening
- Test both success and failure cases - Include tests for panics and out-of-gas scenarios
Next Steps
- Review the Service SDK examples for service implementation patterns
- Explore the jammin suite for more tools and features