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.
The easiest way to start is with the project initializer:
npx as-test initThat gives you a basic config file, a sample test, and optionally a sample fuzzer.
If you already have a project and just want the package:
npm install --save-dev as-testFull documentation lives at:
https://docs.jairus.dev/as-test
Tests usually live in assembly/__tests__/*.spec.ts.
Example:
import { describe, expect, test } from "as-test";
describe("math", () => {
test("adds numbers", () => {
expect(1 + 2).toBe(3, "should add two numbers");
});
});Run everything:
npx ast testRun through the automatic worker pool:
npx ast test --parallelRun one matching file:
npx ast test mathRe-run one suite inside a matching file:
npx ast run math --suite math
npx ast run math --suite math/adds-numbersYou do not need to learn every CLI flag to get started. Most projects can begin with npx ast test, then add more configuration only when they need it.
Mocking is supported, but the idea is to use it sparingly.
With as-test, the ideal path is to run your code against the real runtime and real imports when you can. When that is not practical, you can mock individual imports instead of rebuilding your whole environment around fake behavior.
For local functions, use mockFn and unmockFn. For host imports, use mockImport and unmockImport.
That is especially useful when:
- an import talks to the outside world
- a host function is hard to reproduce in a test
- you want to force an edge case that is difficult to trigger naturally
This keeps tests focused. You can still verify the logic in your AssemblyScript code without needing every runtime dependency to be real in every test. It also pairs well with snapshots when you want to capture the output of a mocked import and make sure it stays stable over time.
Example:
import { describe, expect, mockFn, test, unmockFn } from "as-test";
function getConfig(): string {
return "name=prod\nmode=live";
}
mockFn(getConfig, (): string => "name=demo\nmode=test");
describe("config", () => {
test("reads mocked data", () => {
expect(getConfig()).toContain("demo");
});
});
unmockFn(getConfig);For import mocking, the same idea applies, but it is usually easier to keep the imported function in a small wrapper module and mock that import path from the spec.
Snapshots are useful when the output matters more than the exact step-by-step assertions.
They work well for:
- generated strings or structured text
- serialized values
- the output of mocked imports
- larger results that would be awkward to check field by field
That lets you keep tests readable while still locking down behavior that should not change unexpectedly.
Example:
import { describe, expect, test } from "as-test";
function renderReport(): string {
return "name=demo\nmode=test";
}
describe("report", () => {
test("matches the saved output", () => {
expect(renderReport()).toMatchSnapshot();
});
});The first time you run a snapshot test, create the snapshot with:
npx ast test --create-snapshotsAfter that, a normal npx ast test will verify it.
If an existing snapshot legitimately changed, overwrite it with:
npx ast test --overwrite-snapshotsOne of the main reasons to use as-test is that you are not locked into a single runtime.
If your project runs under WASI, bindings, or a custom runner, you can point your tests at that environment instead of treating Node.js as the only way to execute them.
For example, a simple WASI setup in as-test.config.json can look like this:
{
"input": ["./assembly/__tests__/*.spec.ts"],
"buildOptions": {
"target": "wasi"
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.wasi.js"
}
}
}Then run your tests normally:
npx ast testIf you want to keep a single runtime, one config is enough. If you want to fan out across multiple runtimes, use modes.
Modes let one project keep more than one runtime or build target available at the same time.
For example:
{
"input": ["./assembly/__tests__/*.spec.ts"],
"modes": {
"wasi": {
"default": true,
"buildOptions": {
"target": "wasi"
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.wasi.js"
}
}
},
"bindings": {
"default": true,
"buildOptions": {
"target": "bindings"
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.bindings.js"
}
}
}
}
}Set "default": false on a mode when you want to keep it available for explicit --mode ... runs without including it in normal runs:
{
"modes": {
"web": {
"default": false,
"buildOptions": {
"target": "web"
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.web.js",
"browser": "chromium"
}
}
}
}
}With that setup:
npx ast testruns the root/default config plus any modes whose "default" flag is not false, while:
npx ast test --mode webstill runs the web mode explicitly.
Modes can also be full config objects. That means a mode can override fuzzing, input globs, output aliases, runtime, build flags, and the rest of the normal config surface:
{
"modes": {
"web": {
"fuzz": {
"input": ["./assembly/__fuzz__/web/*.fuzz.ts"],
"runs": 200
},
"buildOptions": {
"target": "web"
},
"runOptions": {
"runtime": {
"cmd": "node ./.as-test/runners/default.web.js",
"browser": "chromium"
}
}
}
}
}If you prefer to keep one mode in a separate file, point the mode directly at that config file:
{
"modes": {
"simd": "./as-test.config.simd.json"
}
}Run a specific mode with:
npx ast test --mode wasior
npx ast test --mode wasi,bindingsCoverage is opt-in.
Enable it from the CLI:
npx ast test --enable coverage
npx ast test --enable coverage --show-coverage
npx ast test --enable coverage --show-coverage=allOr from config:
{
"coverage": {
"enabled": true,
"mode": "project",
"dependencies": ["json-as"],
"includeSpecs": false
}
}Coverage modes:
project- covers project files only
- excludes dependency files by default
all- covers project files and dependency files
- still excludes AssemblyScript stdlib files
If you only want specific dependencies, keep mode: "project" and list package names in dependencies.
That works for both normal installs and pnpm layouts.
--show-coverage prints uncovered point details. --show-coverage=all and --verbose expand nested uncovered gaps instead of collapsing them.
Fuzzers usually live in assembly/__fuzz__/*.fuzz.ts.
Example:
import { expect, FuzzSeed, fuzz } from "as-test";
fuzz("bounded integer addition", (left: i32, right: i32): bool => {
const sum = left + right;
expect(sum - right).toBe(left);
return sum >= i32.MIN_VALUE;
}).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
});Pass a third argument to override the operation count for one target without changing the global fuzz config:
fuzz(
"hot path stays stable",
(): void => {
expect(1 + 1).toBe(2);
},
250,
);Or pass it as the second argument to .generate(...):
fuzz(
"ascii strings survive concatenation boundaries",
(input: string): bool => {
expect(input.length <= 40).toBe(true);
return true;
},
).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
run(seed.string({ charset: "ascii", min: 0, max: 40 }));
}, 250);You can still override fuzz runs from the CLI when you want to force a different count for the current command:
npx ast fuzz --runs 500
npx ast fuzz --runs 1.5x
npx ast fuzz --runs +10%
npx ast fuzz --runs +100000If you used npx ast init with a fuzzer example, the config is already there. Otherwise, add a fuzz block to as-test.config.json so npx ast fuzz knows what to build:
{
"fuzz": {
"input": ["./assembly/__fuzz__/*.fuzz.ts"],
"target": "bindings"
}
}ast fuzz runs fuzz files across the selected modes, reports one result per file, and keeps the final summary separate from the normal test totals. If you want one combined command, use ast test --fuzz.
By default, each fuzz run campaign picks a new random base seed. Pin a seed with --seed <n> (or --fuzz-seed <n> on ast test) when you want deterministic replay.
When a fuzzer fails, as-test now prints the exact failing seeds and one-run repro commands such as ast fuzz ... --seed <seed+n> --runs 1. Crash records in .as-test/crashes also include the captured inputs passed to run(...), which helps when the generator itself has side effects.
Run only fuzzers:
npx ast fuzzRun one matching fuzz target:
npx ast fuzz string --fuzzer ascii-strings-survive-concatenation-boundariesRun tests and fuzzers together:
npx ast test --fuzzFuzzing is there when you want broader input coverage, but it does not get in the way of the normal test flow. You can start with ordinary specs and add fuzzers later.
This is the general idea throughout the project: write tests once, then choose the runtime that matches how your code actually runs.
Runnable example projects live in examples/. They are useful if you want to see complete setups instead of isolated snippets.
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