The Test Runner
Runar ships with a purpose-built test runner that understands contract compilation, script evaluation, and transaction validation. It is built on top of vitest and extends it with contract-aware capabilities — source map integration, script-level tracing, and property-based fuzzing support.
Running Tests from the CLI
The simplest way to run your test suite:
runar test
This discovers all *.test.ts files in the tests/ directory, compiles any referenced contracts, and runs the tests. Under the hood, it invokes vitest with pre-configured transforms that handle .runar.ts imports.
Common Flags
# Run a specific test file
runar test tests/PriceBet.test.ts
The following flags are vitest flags that may be passed through (they are not registered runar CLI flags):
# Run tests in watch mode (re-runs on file change)
runar test --watch
# Run tests matching a name pattern
runar test --filter "settles to Alice"
# Show verbose output with script execution traces
runar test --verbose
# Generate a coverage report
runar test --coverage
# Run tests in parallel (default) or sequentially
runar test --sequence
Exit Codes
| Code | Meaning |
|---|---|
0 | All tests passed |
1 | One or more tests failed |
2 | Configuration or compilation error |
Test Configuration Options
Test behavior is configured through vitest.config.ts (or vite.config.ts). The Runar monorepo’s root vitest.config.ts provides aliases so tests can import runar-testing and runar-compiler by name:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// Directory containing test files
include: ['tests/**/*.test.ts'],
// Timeout per test in milliseconds
testTimeout: 30000,
},
});
Since runar test delegates to vitest, all standard vitest configuration options apply. See the vitest documentation for the full set of options.
Filtering and Selecting Tests
By File
Pass one or more file paths to run specific test files:
runar test tests/PriceBet.test.ts tests/Counter.test.ts
By Test Name
Use --filter to match test names with a substring or regex:
# Substring match
runar test --filter "settles"
# Regex match
runar test --filter "/^PriceBet.*Alice/"
By Tag
Tests can be tagged using vitest’s built-in describe blocks, and filtered by tag:
describe('PriceBet [integration]', () => {
// ...
});
runar test --filter "[integration]"
ScriptVM: Direct Bitcoin Script Execution
For low-level testing, runar-testing exports a ScriptVM that executes raw Bitcoin Script bytecode. This is useful when you need to test compiled output directly or verify script behavior without the TestContract abstraction:
import { ScriptVM, hexToBytes } from 'runar-testing';
const vm = new ScriptVM();
// Execute unlocking script followed by locking script
// ScriptVM.execute() takes Uint8Array arguments, not hex strings
const result = vm.execute(
hexToBytes('4830450221...'), // unlocking script (Uint8Array)
hexToBytes('76a914...88ac') // locking script (Uint8Array)
);
expect(result.success).toBe(true);
expect(result.stack).toEqual([new Uint8Array([1])]); // OP_TRUE left on stack (Uint8Array[])
By default, the ScriptVM mocks signature verification (checkSig always returns true). Provide a checkSigCallback in options to use real signature verification.
RunarInterpreter: Reference Oracle
The RunarInterpreter is a reference implementation that executes the ANF intermediate representation directly, without compiling to Bitcoin Script. It serves as a ground-truth oracle for verifying compiler correctness:
import { RunarInterpreter } from 'runar-testing';
const interpreter = new RunarInterpreter();
const result = interpreter.execute(anfProgram, 'methodName', args);
You rarely use the interpreter directly in application tests. It is primarily used by the Runar project’s own conformance test suite to validate that compiled script behavior matches the interpreted behavior.
Property-Based Fuzzing
Runar integrates with fast-check for property-based testing. The runar-testing package provides contract-specific generators:
import { describe, it } from 'vitest';
import { fc } from '@fast-check/vitest';
import { arbContract, arbStatelessContract } from 'runar-testing';
describe('PriceBet fuzz', () => {
it.prop([arbContract('./contracts/PriceBet.runar.ts')])(
'never panics on random inputs',
(contract) => {
// arbContract generates random valid initial state and method args
const result = contract.callRandom();
// Property: execution should never throw an unhandled exception.
// It can fail (result.success === false) but must not crash.
expect(result).toBeDefined();
}
);
});
Available Generators
arbContract(sourcePath) — Generates a TestContract with random initial state and provides a callRandom() method that invokes a random public method with random arguments of the correct types.
arbStatelessContract(sourcePath) — Same as arbContract but specialized for stateless contracts. It skips state initialization and only generates method arguments.
Both generators respect the contract’s type annotations to produce well-typed random values (e.g., bigint for numeric fields, valid-length hex strings for PubKey).
Differential Fuzzing
Differential fuzzing compares the output of two implementations to find discrepancies. Runar uses this technique to validate that compiled Bitcoin Script produces the same results as the reference interpreter:
import { differentialFuzz } from 'runar-testing';
// Runs the same random programs through compiler+ScriptVM and RunarInterpreter,
// comparing results. Throws on first disagreement.
differentialFuzz('./contracts/PriceBet.runar.ts', { runs: 5000 });
This is primarily a compiler development tool, but contract authors can use it to build confidence that their contract compiles correctly.
Cross-Compiler Conformance
Runar supports four frontend languages (TypeScript, Solidity, Move, Python). The conformance test suite verifies that the same logical contract produces identical compiled output regardless of the source language. It works by comparing SHA-256 hashes of the compiled artifact against golden files:
The conformance suite is run as part of the project’s internal test infrastructure. The golden files live in tests/golden/ and contain the expected SHA-256 hash for each contract/compiler combination. A mismatch indicates a compiler regression or an intentional change that requires updating the golden files.
Understanding Test Output and Reports
A typical test run produces output like this:
runar test v0.1.0
Compiling 3 contracts...
P2PKH.runar.ts 42ms (186 bytes)
PriceBet.runar.ts 118ms (1,204 bytes)
Counter.runar.ts 87ms (892 bytes)
Running 8 tests in 3 files...
PASS tests/P2PKH.test.ts (2 tests) 48ms
PASS tests/PriceBet.test.ts (3 tests) 92ms
FAIL tests/Counter.test.ts (3 tests) 67ms
x Counter > rejects decrement below zero
AssertionError: expected false to be true
Script trace:
OP_1 OP_SUB OP_DUP OP_0 OP_LESSTHAN OP_VERIFY
^^^^^^^^ FAILED
Source: Counter.runar.ts:18 assert(this.count >= 0n)
Test Files 2 passed | 1 failed (3)
Tests 7 passed | 1 failed (8)
Duration 512ms
When --verbose or trace: true is enabled, failed tests include:
- The full Bitcoin Script opcode trace
- Stack contents at each step
- A source map back to the original line of Runar code
Continuous Integration Setup
Runar tests work in any CI environment that supports Node.js. Here is a GitHub Actions example:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install
- run: pnpm runar test --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
For projects using Go or Rust contracts, add the corresponding toolchain setup steps before runar test. The test runner automatically detects the source language and invokes the appropriate frontend compiler.