Unit testing
Learn how to write and run unit tests for your Clarity smart contracts using the Clarinet JS SDK and Vitest.
Unit testing is the process of testing individual components or functions of smart contracts to ensure they work as expected. The Clarinet JS SDK provides a testing framework that allows you to write these tests using the Vitest testing framework, helping you catch bugs and errors early in the development process.
In this guide, you will:
- 1Set up a new Clarinet project with a
defi
contract. - 2Write a unit test covering the
deposit
function. - 3Run tests and generate coverage reports.
Set up a new Clarinet project
Start by creating a new project with the Clarinet CLI. The command below will create a project structure inside of defi
with the necessary files and folders, including the Clarinet JS SDK already set up for testing.
$clarinet new stx-defi$cd stx-defi
After changing into your project directory, run npm install
to install the package dependencies for testing.
$npm install
Since the smart contract code is out of scope for this guide, we are going to use a pre-existing contract. First, generate a new file using the clarinet contract new
command in order to set up your project with the necessary configuration and test files.
$clarinet contract new defi
Now, inside your defi.clar
file, copy and paste the following contract code:
;; Holds the total amount of deposits in the contract, initialized to 0.(define-data-var total-deposits uint u0);; Maps a user's principal address to their deposited amount.(define-map deposits { owner: principal } { amount: uint });; Public function for users to deposit STX into the contract.;; Updates their balance and the total deposits in the contract.(define-public (deposit (amount uint))(let(;; Fetch the current balance or default to 0 if none exists.(current-balance (default-to u0 (get amount (map-get? deposits { owner: tx-sender })))));; Transfer the STX from sender = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM" to recipient = "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.defi (ie: contract identifier on the chain!)".(try! (stx-transfer? amount tx-sender (as-contract tx-sender)));; Update the user's deposit amount in the map.(map-set deposits { owner: tx-sender } { amount: (+ current-balance amount) });; Update the total deposits variable.(var-set total-deposits (+ (var-get total-deposits) amount));; Return success.(ok true)));; Read-only function to get the total balance by tx-sender(define-read-only (get-balance-by-sender)(ok (map-get? deposits { owner: tx-sender })))
Run clarinet check
to ensure that your smart contract is valid and ready for testing.
You can find the full code for this project in this repo.
Test the deposit function
This deposit
function allows users to deposit STX into the contract, updating their balance inside a deposits
map and adding to the total deposits stored in a total-deposits
variable. The key tests we want to cover are that the deposit is successful and that the user's balance, as well as the contract's total deposits, are updated correctly.
Inside of your defi.test.ts
file, replace the boilerplate code and add the following:
import { describe, it, expect } from 'vitest';import { Cl } from '@stacks/transactions';const accounts = simnet.getAccounts();const wallet1 = accounts.get('wallet_1')!;
These imports provide the testing framework and utilities we need. We also get the wallet_1
account, which will act as our test user.
Next, define the test suite and the specific test case:
describe('stx-defi', () => {it('allows users to deposit STX', () => {// Test code will go here});});
This structure comes from our Vitest integration, and it organizes our tests and describes what we're testing. The describe
block groups multiple test cases together, while the it
block represents a single test case.
Now, let's simulate a deposit. Inside of the it
block, define the amount to deposit and call the deposit
function:
const amount = 1000;const deposit = simnet.callPublicFn('defi', 'deposit', [Cl.uint(amount)], wallet1);
This code simulates a deposit by calling the deposit
function, using the callPublicFn
method from the Clarinet JS SDK, in our contract with a specified amount, just as a user would in the real world.
After making the deposit, create an assertion to verify that the call itself was successful and returns an ok
response type with the value true
:
expect(deposit.result).toBeOk(Cl.bool(true));
Run npm run test
to confirm that this test passes.
Let's go over some of the code in this assertion:
expect
is a function from Vitest that makes an assertion about the value we expect to get back from thedeposit
function.
But how do we test against Clarity types and values? This is where the Cl
and toBeOk
helpers come in.
toBeOk
is a custom matcher function built into Vitest that checks if the result of the deposit call is anOk
response, which is a Clarity type. This is important because it confirms that the deposit transaction was processed successfully.Cl
helper is from the@stacks/transactions
package and is used to create Clarity values in JavaScript. In this case, it's used to create a Clarity boolean with the value oftrue
.
To see more custom matcher examples, check out the reference page.
Once we can confirm that the deposit was successful, write a test to verify that the contract's total deposits have been updated correctly.
const totalDeposits = simnet.getDataVar('defi', 'total-deposits');expect(totalDeposits).toBeUint(amount);
Run npm run test
again to confirm that this test also passes.
This check ensures that the contract accepted our deposit without any issues.
Lastly, verify that the user's balance has been updated correctly:
const balance = simnet.callReadOnlyFn('defi', 'get-balance-by-sender', [], wallet1);expect(balance.result).toBeOk(Cl.some(Cl.tuple({amount: Cl.uint(amount),})));
We call the get-balance-by-sender
function and check if it matches the amount we just deposited.
By following these steps, our test comprehensively verifies that the deposit
function works as intended, updating individual balances and total deposits accurately.
Run tests and generate coverage reports
To run your tests, use:
$npm run test
To generate a coverage report, use:
$npm run coverage
This will run your tests and produce a detailed coverage report, helping you identify any untested parts of your contract.