Skip to main content

Writing Effective Unit Tests

The mission of the ckb-js-vm project is to enable developers to write on-chain scripts using a single language: TypeScript. In the previous chapter, we learned how to write on-chain scripts in TypeScript. This chapter will demonstrate how you can also write unit tests in TypeScript, allowing you to use just one language for your entire development workflow.

ckb-testtool

While Rust developers have been using ckb-testtool for testing, we now have a TypeScript version of ckb-testtool available. This tool leverages two important components:

  1. ccc - A transaction assembler written in TypeScript
  2. ckb-debugger - A debugger and execution environment

The workflow is straightforward:

  • Use ccc to assemble transactions in TypeScript, outputting them in JSON format
  • Use ckb-debugger to execute and validate these transactions
  • Write assertions to verify the expected behavior

This combination provides a complete unit testing framework for CKB scripts written in TypeScript.

Examples

describe("example", () => {
test("alwaysSuccess", () => {
const resource = Resource.default();
const tx = Transaction.default();

// deploy a cell with risc-v binary, return a script.
const lockScript = resource.deployCell(
hexFrom(readFileSync(DEFAULT_SCRIPT_ALWAYS_SUCCESS)),
tx,
false
);
// update args
lockScript.args = "0xEEFF";

// mock a input cell with the created script as lock script
const inputCell = resource.mockCell(lockScript);

// add input cell to the transaction
tx.inputs.push(Resource.createCellInput(inputCell));
// add output cell to the transaction
tx.outputs.push(Resource.createCellOutput(lockScript));
// add output data to the transaction
tx.outputsData.push(hexFrom("0x"));

// verify the transaction
const verifier = Verifier.from(resource, tx);
verifier.verifySuccess();
});

test("alwaysFailure", () => {
const resource = Resource.default();
const tx = Transaction.default();

const lockScript = resource.deployCell(
hexFrom(readFileSync(DEFAULT_SCRIPT_ALWAYS_FAILURE)),
tx,
false
);
const inputCell = resource.mockCell(lockScript);
tx.inputs.push(Resource.createCellInput(inputCell));

const verifier = Verifier.from(resource, tx);
verifier.verifyFailure();
verifier.verifyFailure(-1);
});
});

In the example above, we're testing on-chain script with two test cases:

  1. A test case which succeeds
  2. A test case which fails

This pattern allows you to verify both the positive and negative cases for your script's validation logic, ensuring robust behavior in all scenarios.

Pre-compiled Test Binaries

To simplify testing, the ckb-js-vm project provides several pre-compiled binaries that you can use in your test cases:

  1. Always Success Script - A script that always returns success (exit code 0)

    • Access via DEFAULT_SCRIPT_ALWAYS_SUCCESS
  2. Always Failure Script - A script that always returns failure (exit code -1)

    • Access via DEFAULT_SCRIPT_ALWAYS_FAILURE
  3. ckb-js-vm Script - The main ckb-js-vm runtime for testing TypeScript scripts

    • Access via DEFAULT_SCRIPT_CKB_JS_VM

These binaries can be imported directly in your tests without needing to compile them yourself, making it easier to create test fixtures and validation scenarios.

Example usage:

// Import the binary
const alwaysSuccessScript = hexFrom(
readFileSync(DEFAULT_SCRIPT_ALWAYS_SUCCESS)
);

// Deploy it in your test
const lockScript = resource.deployCell(alwaysSuccessScript, tx, false);

⚠️ SECURITY WARNING: These pre-compiled binaries are intended for testing purposes only. Never deploy them in a production environment. For production use, always compile your scripts from source code to ensure security and integrity.