Table of Contents
Most AssemblyScript testing tools are tied to a single runtime, usually Node.js. This works for development, but it doesn’t reflect how your code runs in production. If you deploy to WASI, Wazero, or a custom runtime, you often end up mocking everything and maintaining parallel logic just for tests. as-test solves this by letting you run tests on your actual target runtime, while only mocking what’s necessary.
Key benefits
- Runtime-agnostic: test on WASI, bindings, or custom runners
- Minimal mocking: keep real imports when possible
- Production-like testing: catch runtime-specific issues early
- Inline mocking and snapshots
- Custom reporters and coverage
The installation script will set everything up for you:
npx as-test init --dir ./path-to-installTo scaffold and install dependencies in one step:
npx as-test init --dir ./path-to-install --installAlternatively, you can install it manually:
npm install as-test --save-devFull runnable examples live in examples/, including:
- complete spec files for core features
- import mocking and import snapshot patterns
- mode-based runtime matrix config in
examples/as-test.config.json - a dedicated config you can run directly
See examples/README.md for the walkthrough.
Create assembly/__tests__/math.spec.ts:
import { describe, test, expect, run } from "as-test";
describe("math", () => {
test("addition", () => {
expect(1 + 2).toBe(3);
});
test("close to", () => {
expect(3.14159).toBeCloseTo(3.14, 2);
});
});No selectors:
ast testUses configured input patterns from as-test.config.json.
By name:
ast test sleepResolves to <configured-input-dir>/sleep.spec.ts.
By explicit path or glob:
ast test ./assembly/__tests__/sleep.spec.ts
ast test ./assembly/__tests__/*.spec.tsMultiple selectors:
ast test sleep array ./assembly/__tests__/snapshot.spec.tsComma-separated bare suite names:
ast test box,custom,generics,string
ast run box,custom,generics,string
ast build box,custom,generics,stringIf nothing matches, ast test exits non-zero with:
No test files matched: ...
--config <path>: use another config file--mode <name[,name...]>: run one or multiple named config modes (if omitted andmodesis configured, as-test runs all configured modes)--update-snapshots: write snapshot updates--no-snapshot: disable snapshot assertions for the run--show-coverage: print uncovered coverage points--enable <feature>: enable as-test feature (coverage,try-as)--disable <feature>: disable as-test feature (coverage,try-as)--verbose: keep expanded suite/test lines and update running....statuses in place--clean: disable in-place TTY updates and print only final per-file verdict lines. Useful for CI/CD.--list: show resolved files, per-mode artifacts, and runtime command without executing--list-modes: show configured and selected modes without executing--help/-h: show command-specific help (ast test --help,ast init --help, etc.)
Example:
ast build --enable try-as
ast test --disable coveragePreview execution plan:
ast test --list
ast test --list-modes
ast run sleep --list --mode wasi
ast build --list --mode wasi,bindingsUse ast doctor to validate local setup before running tests.
ast doctorYou can also target specific modes and config files:
ast doctor --config ./as-test.config.json --mode wasi,bindingsdoctor checks:
- config file loading and mode resolution
- required dependencies (for example
assemblyscript,@assemblyscript/wasi-shimfor WASI targets) - runtime command parsing and executable availability
- runtime script path existence (for script-host runtimes)
- test spec file discovery from configured input patterns
If any ERROR checks are found, ast doctor exits non-zero.
Use these helpers when you need to replace behavior during tests:
mockFn(oldFn, newFn): rewrites subsequent calls tooldFnin the same spec file to usenewFnunmockFn(oldFn): stops that rewrite for subsequent callsmockImport("module.field", fn): sets the runtime mock for an external importunmockImport("module.field"): clears the runtime mock for an external importsnapshotImport<T = Function | string>(imp: T, version: string | i32): snapshots a single import mocksnapshotImport<T = Function | string>(imp: T, capture: () => unknown): runscaptureand snapshots using version"default"restoreImport<T = Function | string>(imp: T, version: string | i32): restores a single import mock
Example:
import {
expect,
it,
mockFn,
mockImport,
restoreImport,
run,
snapshotImport,
unmockFn,
unmockImport,
} from "as-test";
import { foo } from "./mock";
mockImport("mock.foo", (): string => "buz");
mockFn(foo, (): string => "baz " + foo());
it("mocked function", () => {
expect(foo()).toBe("baz buz");
});
unmockFn(foo);
it("function restored", () => {
expect(foo()).toBe("buz");
});
snapshotImport(foo, 1);
mockImport("mock.foo", (): string => "temp");
snapshotImport("mock.foo", "v2");
restoreImport(foo, 1);
snapshotImport("mock.foo", () => foo()); // snapshots to version "default"
restoreImport("mock.foo", "default");
unmockImport("mock.foo");
mockImport("mock.foo", (): string => "buz");
run();Snapshot assertions are enabled by default.
- Read-only mode (default): missing/mismatched snapshots fail
- Update mode:
--update-snapshotswrites missing/mismatched snapshots
Commands:
ast test --update-snapshots
ast run --update-snapshots
ast test --no-snapshotSnapshot files are stored in snapshotDir (default ./.as-test/snapshots).
Coverage is controlled by coverage in config.
Coverage reporting includes source files ending in .ts or .as only.
- Boolean form:
true/false
- Object form:
{ "enabled": true, "includeSpecs": false }
Default behavior includes non-spec files and excludes *.spec.ts files.
Show point-level misses:
ast test --show-coverageCoverage artifacts:
ast runwritescoverage.log.jsontocoverageDir(if enabled and not"none")ast testwrites per-file coverage artifacts (coverage.<file>.log.json)
Log artifacts:
ast runwritestest.log.jsontologs(iflogsis not"none")ast testwrites per-file logs (test.<file>.log.json)
Default file: as-test.config.json
Example:
{
"$schema": "./as-test.config.schema.json",
"input": ["./assembly/__tests__/*.spec.ts"],
"output": "./.as-test/",
"config": "none",
"coverage": true,
"env": {},
"buildOptions": {
"cmd": "",
"args": [],
"target": "wasi"
},
"modes": {},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.wasi.js <file>"
},
"reporter": ""
}
}Key fields:
input: glob list of spec filesoutput: output alias. Use a root string ("./.as-test/") or object ({ "build": "...", "logs": "...", "coverage": "...", "snapshots": "..." })outDir: compiled wasm output dirlogs: log output dir or"none"coverageDir: coverage output dir or"none"snapshotDir: snapshot storage diroutDir,logs,coverageDir, andsnapshotDirstill work; when both are set, these explicit fields overrideoutputenv: environment variables injected into build and runtime processesbuildOptions.cmd: optional custom build command template; when set it replaces default build command and flags. Supports<file>,<name>,<outFile>,<target>,<mode>buildOptions.target:wasiorbindingsmodes: named overrides for command/target/args/runtime/env/artifact directories (selected via--mode);mode.envoverrides top-levelenvrunOptions.runtime.cmd: runtime command, supports<file>and<name>; if its script path is missing, as-test falls back to the default runner for the selected targetrunOptions.reporter: reporter selection as a string or object
Validation behavior:
- Config parsing is strict for
ast build,ast run,ast test, andast doctor. - Invalid JSON fails early with parser details (
line/columnwhen provided by Node). - Unknown properties are rejected and include a nearest-key suggestion when possible.
- Invalid property types are reported with their JSON path and a short fix hint.
- On validation failure, the command exits non-zero and prints
run "ast doctor" to check your setup.
Example validation error:
invalid config at ./as-test.config.json
1. $.inpoot: unknown property
fix: use "input" if that was intended, otherwise remove this property
2. $.runOptions.runtime.cmd: must be a string
fix: set to a runtime command including "<file>"
run "ast doctor" to check your setup.
Example multi-runtime matrix:
{
"modes": {
"wasi-simd": {
"buildOptions": {
"target": "wasi",
"args": ["--enable", "simd"]
},
"runOptions": {
"runtime": {
"cmd": "wasmer run <file>"
}
}
},
"wasi-nosimd": {
"buildOptions": {
"target": "wasi"
},
"runOptions": {
"runtime": {
"cmd": "wasmer run <file>"
}
}
},
"bindings-node-simd": {
"buildOptions": {
"target": "bindings",
"args": ["--enable", "simd"]
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.bindings.js <file>"
}
}
}
}
}Run all modes:
ast test --mode wasi-simd,wasi-nosimd,bindings-node-simdSummary totals:
Modesin the default reporter is config-scoped (totalis all configured modes)- when selecting fewer modes with
--mode, unselected modes are counted asskipped Filesin the default reporter is also config-scoped (totalis all files from configured input patterns)- when selecting fewer files, unselected files are counted as
skipped
When using --mode, compiled artifacts are emitted as:
<test-name>.<mode>.<target>.wasm
Example:
math.wasi-simd.wasi.wasm
math.bindings-node-simd.bindings.wasm
Bindings runner naming:
- preferred:
./.as-test/runners/default.bindings.js - deprecated but supported:
./.as-test/runners/default.run.js
ast init now scaffolds both local runners:
.as-test/runners/default.wasi.js.as-test/runners/default.bindings.js
Built-in TAP reporter (useful for CI, including GitHub Actions):
ast run --reporter tapTAP output is written to ./.as-test/reports/report.tap by default.
Or in config:
{
"runOptions": {
"reporter": "tap"
}
}Or with reporter object config:
{
"runOptions": {
"reporter": {
"name": "tap",
"options": ["single-file"],
"outDir": "./.as-test/reports"
}
}
}options supports single-file (default) and per-file.
Single-file explicit path:
{
"runOptions": {
"reporter": {
"name": "tap",
"outFile": "./.as-test/reports/report.tap"
}
}
}In GitHub Actions, failed TAP points emit ::error annotations with file and line when available.
Example GitHub workflow (Bun + Wasmtime + TAP summary):
name: Run Tests
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jcbhmr/setup-wasmtime@v2
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun run test --update-snapshots --tap
- uses: test-summary/action@v2
if: always()
with:
paths: ".as-test/reports/*.tap"Set reporter path in config:
{
"runOptions": {
"reporter": "./tests/my-reporter.js"
}
}It's even possible to use something like tap-summary to summarize the test results!
npm install -g tap-summary
ast test --reporter tap | tap-summaryReporter module should export createReporter (named or default):
export function createReporter(context) {
return {
onRunStart(event) {},
onFileStart(event) {},
onFileEnd(event) {},
onSuiteStart(event) {},
onSuiteEnd(event) {},
onAssertionFail(event) {},
onSnapshotMissing(event) {},
onRunComplete(event) {},
};
}With these hooks, you can emit machine-readable output (for example TAP/JSON) while still keeping the default human-readable terminal view for local runs.
Skip helpers:
xdescribe(name, fn)xtest(name, fn)xit(name, fn)xexpect(value)
Available matchers:
toBe(expected)toBeNull()toBeGreaterThan(value)toBeGreaterOrEqualTo(value)toBeLessThan(value)toBeLessThanOrEqualTo(value)toBeString()toBeBoolean()toBeArray()toBeNumber()toBeInteger()toBeFloat()toBeFinite()toBeTruthy()toBeFalsy()toBeCloseTo(expected, precision = 2)toMatch(substring)toStartWith(prefix)toEndWith(suffix)toHaveLength(length)toContain(itemOrSubstring)(toContainsalias supported)toThrow()(withtry-as)toMatchSnapshot(name?)
This project is distributed under an open source license. Work on this project is done by passion, but if you want to support it financially, you can do so by making a donation to the project's GitHub Sponsors page.
You can view the full license using the following link: License
Please send all issues to GitHub Issues and to converse, please send me an email at me@jairus.dev
- Email: Send me inquiries, questions, or requests at me@jairus.dev
- GitHub: Visit the official GitHub repository Here
- Website: Visit my official website at jairus.dev
- Discord: Contact me at My Discord or on the AssemblyScript Discord Server