Skip to main content

Coverage collection

This page covers coverage calculated on TVM assembly instructions. Path- and source-line coverage is not implemented.
There are two main ways to calculate coverage of your @ton/sandbox tests.

Easy way

Library compatibilityFor this way to work correctly, version of @ton/sandbox >= 0.37.2 and @ton/blueprint >= 0.41.0 is needed.
When using blueprint, the only thing you need to collect coverage is to run
blueprint test --coverage
Results will appear in coverage/ directory as HTML files with reports for each of your contracts.

Customizable way

There might be some reasons why you don’t want to simply use --coverage.
  • You don’t want to collect coverage for all contracts.
  • You use @ton/sandbox but don’t use @ton/blueprint.
  • Not all contracts have source code. (For example, for each transaction you deploy a new contract, and you don’t have wrappers for it).
  • You want to get the raw data and customize the output.

1. Enable coverage collection

Before running tests, add blockchain.enableCoverage() to collect coverage data:
import {Blockchain} from '@ton/sandbox';

describe('Contract Tests', () => {
    let blockchain: Blockchain;
    let contract: SandboxContract<MyContract>;

    beforeEach(async () => {
        blockchain = await Blockchain.create();

        blockchain.enableCoverage();
        // or for COVERAGE=true mode only
        // blockchain.enableCoverage(process.env["COVERAGE"] === "true");

        // Deploy your contract
        contract = blockchain.openContract(MyContract.fromInit());
        // ... deployment logic
    });

    // Your tests here...
});

2. Collect coverage after tests

afterAll(() => {
    const coverage = blockchain.coverage(contract);
    console.log(coverage?.summary());
})

3. Generate reports

import {writeFileSync} from 'fs';

afterAll(() => {
    const coverage = blockchain.coverage(contract);
    if (!coverage) return;

    // Generate HTML report for detailed analysis
    const htmlReport = coverage.report("html");
    writeFileSync("coverage.html", htmlReport);

    // Print text text report to console
    const textReport = coverage.report("text");
    console.log(textReport);
});

Understanding coverage data

Coverage summary

The coverage summary provides key metrics about your test coverage:
const summary = coverage.summary();

console.log(`Total lines: ${summary.totalLines}`);
console.log(`Covered lines: ${summary.coveredLines}`);
console.log(`Coverage percentage: ${summary.coveragePercentage.toFixed(2)}%`);
console.log(`Total gas consumed: ${summary.totalGas}`);
console.log(`Total hits: ${summary.totalHits}`);

// Instruction-level statistics
summary.instructionStats.forEach(stat => {
    console.log(`${stat.name}: ${stat.totalHits} hits, ${stat.totalGas} gas, avg ${stat.avgGas}`);
});

Coverage reports

  • HTML Report: Interactive report with highlighting and line-by-line coverage details
  • Text Report: Console-friendly report with coverage information and marked code

Advanced usage patterns

Multiple test suites

When running multiple test files, you might want to merge coverage data:
// In first test file
const coverage1 = blockchain.coverage(contract);
if (!coverage1) return;
const coverage1Json = coverage1.toJson();
writeFileSync("coverage1.json", coverage1Json);

// In second test file
const coverage2 = blockchain.coverage(contract);
if (!coverage2) return;
const coverage2Json = coverage2.toJson();
writeFileSync("coverage2.json", coverage2Json);

// Merge coverage data in separate script after tests
const savedCoverage1 = Coverage.fromJson(readFileSync("coverage1.json", "utf-8"));
const savedCoverage2 = Coverage.fromJson(readFileSync("coverage2.json", "utf-8"));
const totalCoverage = savedCoverage1.mergeWith(savedCoverage2);

console.log(`Combined coverage: ${totalCoverage.summary().coveragePercentage}%`);

Coverage for multiple contracts

When testing systems with multiple contracts:
not runnable
describe('Multi-Contract System', () => {
    let blockchain: Blockchain;
    let contract1: SandboxContract<Contract1>;
    let contract2: SandboxContract<Contract2>;

    beforeEach(async () => {
        blockchain = await Blockchain.create();
        blockchain.enableCoverage();

        // Deploy multiple contracts
        contract1 = blockchain.openContract(Contract1.fromInit());
        contract2 = blockchain.openContract(Contract2.fromInit());
    });

    afterAll(() => {
        // Get coverage for each contract separately
        const coverage1 = blockchain.coverage(contract1);
        const coverage2 = blockchain.coverage(contract2);

        if (!coverage1 || !coverage2) return;

        console.log('Contract 1 Coverage:', coverage1.summary().coveragePercentage);
        console.log('Contract 2 Coverage:', coverage2.summary().coveragePercentage);

        // Generate separate reports
        writeFileSync("contract1-coverage.html", coverage1.report("html"));
        writeFileSync("contract2-coverage.html", coverage2.report("html"));
    });
});

Interpret results

The usual report looks like this: Apart from the header statistics, the line-by-line coverage report is the most informative. Most fields are self‑explanatory; the code section shows per‑instruction hit counts (blue) and gas cost (red). This helps you analyze both coverage and gas efficiency.
To understand the TVM assembly output, read TVM.

Limitations

Note that when code of other contracts is stored directly in the code of contract (Tact does that automatically if a contract system does not contain circular dependencies), that affects the overall code coverage percentage. To mitigate this effect in coverage estimation, add a circular dependency. For example, import a file with the following content.
contract A {
    receive() {
        let x = initOf B();
        drop2(x);
    }
}

contract B(
) {
    receive() {
        let x = initOf A();
        drop2(x);
    }
}

asm fun drop2(x: StateInit) {
    DROP2
}
I