Skip to main content

Test Contract

Clarinet supports automatic testing, where your blockchain application requirements can be converted to test cases. Clarinet comes with a testing harness based on Deno that applies the unit tests you write in TypeScript to your smart contracts.

Topics covered in this guide:

Clarity contracts and unit tests

Let us consider a counter smart contract to understand how to write unit tests for our application requirements.

;; counter
(define-data-var counter uint u1) ;; counter initialized to 1

(define-public (increment (step uint)) ;; increment counter, print new-val
(let ((new-val (+ step (var-get counter))))
(var-set counter new-val)
(print { object: "counter", action: "incremented", value: new-val })
(ok new-val)))

(define-public (decrement (step uint)) ;; decrement counter, print new-val
(let ((new-val (- step (var-get counter))))
(var-set counter new-val)
(print { object: "counter", action: "decremented", value: new-val })
(ok new-val)))

(define-read-only (read-counter) ;; read value of counter
(ok (var-get counter)))

Our counter application keeps track of an initialized value, allows for incrementing and decrementing, and prints actions as a log. Let us turn these requirements into unit tests.

Unit tests for counter example

When you created your Clarity contract with clarinet contract new <my-project>, Clarinet automatically created a test file for the contract within the tests directory: tests/my-projects_test.ts. Other files under the tests/ directory following the Deno test naming convention will also be included:

  • named test.{ts, tsx, mts, js, mjs, jsx, cjs, cts},
  • or ending with .test.{ts, tsx, mts, js, mjs, jsx, cjs, cts},
  • or ending with _test.{ts, tsx, mts, js, mjs, jsx, cjs, cts}

Within these tests, developers can simulate mining a block containing transactions using their contract and then examine the results of those transactions and the events generated by them.

NOTE:

If you see an error in Visual Studio Code (VS Code) on the imports in the generated test file(s) that says, "An import path cannot end with a '.ts' extension" (example below), follow the below steps to resolve the error: VS Code deno error

  • Install the Deno extension in VS Code
  • Install Deno on your computer
  • In VS Code, open the command palette (Ctrl+Shift+P in Windows; Cmd+Shift+P on Mac) and run the Deno: Initialize Workspace Configuration and Deno: Cache Dependencies commands
  • Open Command Prompt (Terminal on a Mac); navigate to the tests folder in your project and run deno run test-file-name.ts (Make sure to replace test-file-name with the actual name of the test file, counter_test.ts in the current example )
  • Quit and restart VS Code

Clarinet allows you to instantly initialize wallets and populate them with tokens, which helps to interactively or programmatically test the behavior of the smart contract. Blocks are mined instantly, so you can control the number of blocks that are mined between testing transactions.

To define a Clarinet test, you need to register it with a call to Clarinet.test(). In the example unit test below, you see us

  1. Importing the relevant classes from the Clarinet module on Deno
  2. Instantiating and passing common Clarinet objects to our Clarinet.test() API call
  3. Defining a user wallet_1, calling increment, and asserting its results
// counter_test.ts - A unit test file
import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v1.0.5/index.ts';

Clarinet.test({
name: "Ensure that increment works.",
async fn(chain: Chain, accounts: Map<string, Account>) {
let wallet_1 = accounts.get("wallet_1")!; // instantiate a user

let block = chain.mineBlock([
Tx.contractCall("counter", "increment", [types.uint(3)], wallet_1.address) // increment counter by 3
]);

block.receipts[0].result // ensure that counter returned 3
.expectOk()
.expectUint(3)
},
});

We run this test with

clarinet test

For a complete list of classes, objects, and interfaces available, see Deno's Clarinet module index.

You can watch a step-by-step walkthrough of using clarinet test and watch Executing Tests and Checking Code Coverage.

Comprehensive unit tests for counter

Let us now write a higher coverage test suite.

// counter_test.ts - a comprehensive unit test file
import { Clarinet, Tx, Chain, Account, Contract, types } from 'https://deno.land/x/clarinet@v1.0.2/index.ts';
import { assertEquals } from "https://deno.land/std@0.90.0/testing/asserts.ts";

Clarinet.test({
name: "Ensure that counter can be incremented multiples per block, accross multiple blocks",
async fn(chain: Chain, accounts: Map<string, Account>, contracts: Map<string, Contract>) {
let wallet_1 = accounts.get("wallet_1")!;
let wallet_2 = accounts.get("wallet_2")!; // multiple users

let block = chain.mineBlock([
Tx.contractCall("counter", "increment", [types.uint(1)], wallet_1.address),
Tx.contractCall("counter", "increment", [types.uint(4)], wallet_1.address),
Tx.contractCall("counter", "increment", [types.uint(10)], wallet_1.address)
]); // multiple contract calls

assertEquals(block.height, 2); // asserting block height
block.receipts[0].result // checking log for expected results
.expectOk()
.expectUint(2);
block.receipts[1].result
.expectOk()
.expectUint(6);
block.receipts[2].result
.expectOk()
.expectUint(16);

block = chain.mineBlock([
Tx.contractCall("counter", "increment", [types.uint(1)], wallet_1.address),
Tx.contractCall("counter", "increment", [types.uint(4)], wallet_1.address),
Tx.contractCall("counter", "increment", [types.uint(10)], wallet_1.address),
Tx.transferSTX(1, wallet_2.address, wallet_1.address),
]); // more contract calls, and an STX transfer

assertEquals(block.height, 3);
block.receipts[0].result
.expectOk()
.expectUint(17);
block.receipts[1].result
.expectOk()
.expectUint(21);
block.receipts[2].result
.expectOk()
.expectUint(31);

let result = chain.getAssetsMaps(); // asserting account balances
assertEquals(result.assets["STX"][wallet_1.address], 99999999999999);

let call = chain.callReadOnlyFn("counter", "read-counter", [], wallet_1.address)
call.result
.expectOk()
.expectUint(31); // asserting a final counter value

"0x0001020304".expectBuff(new Uint8Array([0, 1, 2, 3, 4])); // asserting buffers
"ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.plaid-token".expectPrincipal('ST1HTBVD3JG9C05J7HBJTHGR0GGW7KXW28M5JS8QE.plaid-token'); // asserting principals
},
});

Here, variously, we:

  • instantiated multiple accounts
  • called functions across multiple blocks
  • asserted block heights between transactions
  • tested transfers and balances

Measure and increase code coverage

To help developers maximizing their test coverage, Clarinet can produce a lcov report, using the following option:

clarinet test --coverage

From there, you can use the lcov tooling suite to produce HTML reports.

brew install lcov
genhtml --branch-coverage -o coverage coverage.lcov
open coverage/index.html

lcov

Cost optimization

Clarinet can also be used for optimizing costs. When you execute a test suite, Clarinet keeps track of all costs being computed when executing the contract-call, and display the most expensive ones in a table:

clarinet test --cost

The --cost option can be used in conjunction with --watch and filters to maximize productivity, as illustrated here:

costs

Load contracts in a console

The Clarinet console is an interactive Clarity Read, Evaluate, Print, Loop (REPL) console that runs in-memory. Any contracts in the current project are automatically loaded into memory.

clarinet console

You can use the ::help command in the console for a list of valid commands, which can control the state of the REPL chain, and let you advance the chain tip. Additionally, you may enter Clarity commands into the console and observe the result of the command.

You may exit the console by pressing Ctrl + C twice.

Changes to contracts are not loaded into the console while it is running. If you make any changes to your contracts you must exit the console and run it again.

Spawn a local Devnet

You can use Clarinet to deploy your contracts to your own local offline environment for testing and evaluation on a blockchain.

Use the following command:

clarinet integrate

Make sure that you have a working installation of Docker running locally.

Interacting with contracts deployed on Mainnet

Composition and interactions between protocols and contracts are one of the key innovations in blockchains. Clarinet was designed to handle these types of interactions.

Before referring to contracts deployed on Mainnet, they should be explicitily be listed as a requirement in the manifest Clarinet.toml, either manually:

[project]
name = "my-project"
[[project.requirements]]
contract_id = "SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.bitcoin-whales"

or with the command:

clarinet requirements add SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.bitcoin-whales

From there, Clarinet will be able to resolve the contract-call? statements invoking requirements present in your local contracts by downloading and caching a copy of these contracts and using them during the execution of your test suites, in addition to all the different features available in clarinet.

When deploying your protocol to Devnet / Testnet, for the contracts involving requirements, the setting remap_requirements in your deployment plans must be set.

As a step-by-step example, we use here the following contract, bitcoin-whales

If you examine this contract, you will see that there are 3 different dependencies: two from the same project (included in the same Clarinet.toml file), and one referring to a contract deployed outside of the current project.

Same Project

In the contract snippet below (line:260-265), there are dependencies on the contracts conversion and conversion-v2 which are included in the same Clarinet.toml file.

(define-read-only (get-token-uri (token-id uint))
(if (< token-id u5001)
(ok (some (concat (concat (var-get ipfs-root) (unwrap-panic (contract-call? .conversion lookup token-id))) ".json")))
(ok (some (concat (concat (var-get ipfs-root) (unwrap-panic (contract-call? .conversion-v2 lookup (- token-id u5001)))) ".json")))
)
)

External Deployer

In this snippet, there is a dependency on the nft-trait (line:001) deployed by 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.

(impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait)

Dependencies from external contracts should be set in [[project.requirements]].

[project]
name = "my-project"
[[project.requirements]]
contract_id = "SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait"
boot_contracts = ["pox", "costs-v2", "bns"]
[project.cache_location]
path = ".requirements"
[contracts.bitcoin-whales]
path = "contracts/bitcoin-whales.clar"
[contracts.conversion]
path = "contracts/conversion.clar"
[contracts.conversion-v2]
path = "contracts/conversion-v2.clar"
[repl]
costs_version = 2
parser_version = 2
[repl.analysis]
passes = ["check_checker"]
[repl.analysis.check_checker]
strict = false
trusted_sender = false
trusted_caller = false
callee_filter = false

As a next step, we may generate a deployment plan for this project.

If running clarinet integrate for the first time, this file should be created by Clarinet.

In addition, you may run clarinet deployment generate --devnet to create or overwrite.

---
id: 0
name: Devnet deployment
network: devnet
stacks-node: "http://localhost:20443"
bitcoin-node: "http://devnet:devnet@localhost:18443"
plan:
batches:
- id: 0
transactions:
- requirement-publish:
contract-id: SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait
remap-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
remap-principals:
SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 4680
path: ".requirements\\SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.clar"
- contract-publish:
contract-name: conversion
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 340250
path: "contracts\\conversion.clar"
anchor-block-only: true
- contract-publish:
contract-name: conversion-v2
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 351290
path: "contracts\\conversion-v2.clar"
anchor-block-only: true
- contract-publish:
contract-name: bitcoin-whales
expected-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
cost: 87210
path: "contracts\\bitcoin-whales.clar"
anchor-block-only: true

As you can see from the example above, Clarinet will remap the external contract to our Devnet address. In addition, Clarinet will also create a copy of it in the folder requirements

Use Clarinet in your CI workflow as a GitHub Action

Clarinet may be used in GitHub Actions as a step of your CI workflows. You may set-up a simple workflow by adding the following steps in a file .github/workflows/github-actions-clarinet.yml:

name: CI
on: [push]
jobs:
tests:
name: "Test contracts with Clarinet"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: "Execute unit tests"
uses: docker://hirosystems/clarinet:latest
with:
args: test --coverage --manifest-path=./Clarinet.toml
- name: "Export code coverage"
uses: codecov/codecov-action@v1
with:
files: ./coverage.lcov
verbose: true

Or add the steps above in your existing workflows. The generated code coverage output can then be used as is with GitHub Apps like https://codecov.io.