Skip to main content

Framework

This presents the technical framework for testing a smart contract (written in Archetype or Michelson): tests are written in Typescript and executed with Node.js.

Runtime and language

The benefits of Node.js and the npm (yarn) packaging system is the access to a huge ecosystem of tools and utilities to implement tests.

The choice of Typescript (TS) language is motivated by its strongly-typed aspect. A typed language helps validating the code at compilation time and reduce the effort and time to production.

Moreover it is fully-equipped in IDEs (like VSCode, IntelliJ, ...) with inlined code information (function docs and signatures, code navigation and refactoring, ...) and linters to detect issues at the time of coding, rather than at test execution, or even worse, after the contract is deployed.

Contract Binding

The key feature of this test framework is the automatic contract's binding generation. The binding represents the contract in the Typescript world and provides the same API as the contract (storage, entry points, views). Hence invoking the contract is done via the contract's binding.

For example, consider the following Archetype contract:

bind_demo.arl
archetype bind_demo

variable total : nat = 0

entry increase(values : list<bool * nat>) {
for p in values do
total += p[0] ? p[1] : 0
done
}
danger

It is mandatory that the contract identifier be the same are the contract filename.

The generated binding object provides, among other utilities, the following Typescript methods:

  • deploy(params: Partial<ex.Parameters>): Promise<void>
  • increase(values: Array<[ boolean, Nat]>, params: Partial<Parameters>): Promise<any>
  • get_total(): Promise<Nat>

These methods may be called by the test:

test.ts
import { bind_demo }   from './binding/bind_hello'
import { Nat } from "@completium/archetype-ts-types";
import { get_account } from "@completium/experiment-ts";
const assert = require('assert')

const alice = get_account('alice');
/* Deploy contract */
await bind_demo.deploy({ as: alice })
/* Invoke 'increase' entry point */
await bind_demo.increase([[ true, new Nat(2) ], [ false, new Nat(3) ]])
/* Retrieve storage's total value */
const total = await bind_demo.get_total()
/* Assert retrieved value */
assert(total.equals(new Nat(2)))

The main benefit of the binding interface is that the contract interface is known at coding time.

For example, if the wrong type of argument is passed to the increase method, an error is thrown instantly in the IDE (like VSCode below):

Buld DAppBuld DApp

It is then possible to display the expected type of argument by hovering the method:

Buld DAppBuld DApp

See the binding document for a detailed presentation of the generated API.

Michelson execution

In a test environement, the contract is executed locally with the mockup mode of the Octez distribution's client (named tezos-client, see install instructions). It is hence necessary to install it and inform Completium CLI about its path.

The benefit of the mockup mode is that it is uses the same execution engine as the tezos node. This removes the risk of a difference of exectuion semantics between test and production.

Test library

It is best practise, and recommended, to use a generic purpose test library like Mocha or Jest.

It helps structuring test scenarios in units and sequence of units; it provides a clear output of which tests are passing and which are not, with corresponding errors.

It can be combined with a code covering package to check whether all binding services are covered by tests.

See the Mocha section for more information.

Completium packages

The framework relies on several packages:

There is no need to install these packages manually when the create project command is used. See here for more info.

info

Note that it is also possible to use the binding generation in a DApp. See here for a full DApp example.