Usage
This chapter explains how to use Rendezvous in different situations. By the end, you’ll know when and how to use its features effectively.
What’s Inside
Discarding Property-Based Tests
Running Rendezvous
To run Rendezvous, use the following command:
rv <path-to-clarinet-project> <contract-name> <type> [--config] [--seed] [--runs] [--regr] [--bail] [--dial]
Let’s break down each part of the command.
Positional Arguments
Consider this example Clarinet project structure:
root
├── Clarinet.toml
├── contracts
│ └── contract.clar
└── settings
└── Devnet.toml
1. Path to the Clarinet Project
The <path-to-clarinet-project> is the relative or absolute path to the root directory of the Clarinet project. This is where the Clarinet.toml file exists. It is not the path to the Clarinet.toml file itself.
For example, if you’re in the parent directory of root, the correct relative path would be:
rv ./root <contract-name> <type>
2. Contract Name
The <contract-name> is the name of the contract to be tested, as defined in Clarinet.toml.
For example, if Clarinet.toml contains:
[contracts.contract]
path = "contracts/contract.clar"
To test the contract named contract, you would run:
rv ./root contract <type>
3. Testing Type
The <type> argument specifies the testing technique to use. The available options are:
test– Runs property-based tests.invariant– Runs invariant tests.
For a deeper understanding of these techniques and when to use each, see Testing Methodologies.
Running property-based tests
Property-based testing requires one or more test functions (e.g. test-xyz) in the contract file (contract.clar), annotated with #[env(simnet)].
To run property-based tests, use:
rv ./root contract test
This tells Rendezvous to:
- Load the Clarinet project located in
./root. - Target the contract named
contractas defined inClarinet.tomlby executing property-based tests defined within the contract.
Running invariant tests
Invariant testing requires two things in the contract file (contract.clar), both annotated with #[env(simnet)]:
- One or more invariant functions (e.g.
invariant-xyz). - The Rendezvous context — the
contextmap andupdate-contextfunction (see The Rendezvous Context).
To run invariant tests, use:
rv ./root contract invariant
With this command, Rendezvous will:
- Randomly execute public function calls in the
contractcontract. - Randomly check the defined invariants to ensure the contract’s internal state remains valid.
If an invariant check fails, it means the contract’s state has deviated from expected behavior, revealing potential bugs.
Options
Rendezvous also provides additional options to customize test execution:
1. Customizing the Number of Runs
By default, Rendezvous runs 100 test iterations. You can modify this using the --runs option:
rv root contract test --runs=500
This increases the number of test cases to 500.
2. Replaying a Specific Sequence of Events
To reproduce a previous test sequence, you can use the --seed option. This ensures that the same random values are used across test runs:
rv root contract test --seed=12345
How to Find the Replay Seed
When Rendezvous detects an issue, it includes the seed needed to reproduce the test in the failure report. Here’s an example of a failure report with the seed:
Error: Property failed after 2 tests.
Seed : 426141810
Counterexample:
...
What happened? Rendezvous went on a rampage and found a weak spot:
...
In this case, the seed is 426141810. You can use it to rerun the exact same test scenario:
rv root contract test --seed=426141810
3. Stop After First Failure
By default, Rendezvous will start the shrinking process after finding a failure. To stop immediately when the first failure is detected, use the --bail option:
rv root contract test --bail
This is useful when you want to examine the first failure without waiting for the complete test run and shrinking process to finish.
4. Using Dialers
Dialers allow you to define pre- and post-execution functions using JavaScript during invariant testing. To use a custom dialer file, run:
rv root contract invariant --dial=./custom-dialer.js
A good example of a dialer can be found in the Rendezvous repository, within the example Clarinet project, inside the sip010.cjs file.
In that file, you’ll find a post-dialer designed as a sanity check for SIP-010 token contracts. It ensures that the transfer function correctly emits the required print event containing the memo, as specified in SIP-010.
How Dialers Work
During invariant testing, Rendezvous picks up dialers when executing public function calls:
- Pre-dialers run before each public function call.
- Post-dialers run after each public function call.
Both have access to an object containing:
selectedFunction– The function being executed.functionCall– The result of the function call (undefinedfor pre-dialers).clarityValueArguments– The generated Clarity values used as arguments.
Example: Post-Dialer for SIP-010
Below is a post-dialer that verifies SIP-010 compliance by ensuring that the transfer function emits a print event containing the memo.
async function postTransferSip010PrintEvent(context) {
const { selectedFunction, functionCall, clarityValueArguments } = context;
// Ensure this check runs only for the "transfer" function.
if (selectedFunction.name !== "transfer") return;
const functionCallEvents = functionCall.events;
const memoParameterIndex = 3; // The memo parameter is the fourth argument.
const memoGeneratedArgumentCV = clarityValueArguments[memoParameterIndex];
// If the memo argument is `none`, there's nothing to validate.
if (memoGeneratedArgumentCV.type === "none") return;
// Ensure the memo argument is an option (`some`).
if (memoGeneratedArgumentCV.type !== "some") {
throw new Error("The memo argument must be an option type!");
}
// Convert the `some` value to hex for comparison.
const hexMemoArgumentValue = cvToHex(memoGeneratedArgumentCV.value);
// Find the print event in the function call events.
const sip010PrintEvent = functionCallEvents.find(
(ev) => ev.event === "print_event",
);
if (!sip010PrintEvent) {
throw new Error(
"No print event found. The transfer function must emit the SIP-010 print event containing the memo!",
);
}
const sip010PrintEventValue = sip010PrintEvent.data.raw_value;
// Validate that the emitted print event matches the memo argument.
if (sip010PrintEventValue !== hexMemoArgumentValue) {
throw new Error(
`Print event memo value does not match the memo argument: ${hexMemoArgumentValue} !== ${sip010PrintEventValue}`,
);
}
}
This dialer ensures that any SIP-010 token contract properly emits the memo print event during transfers, helping to catch deviations from the standard.
5. Regression Testing
Rendezvous automatically saves failing test cases to prevent regressions. When a test fails, the seed and configuration are persisted to disk. On subsequent runs, you can replay these failures to ensure bugs stay fixed.
Default Behavior
By default, Rendezvous runs fresh random tests:
rv root contract test
This explores new test cases using the provided configuration (seed, dial, etc.).
Running Regression Tests
To verify that previously discovered bugs remain fixed, use the --regr flag:
rv root contract test --regr
Rendezvous will load all saved failures for the contract and replay them using their original seeds. This ensures that fixed bugs stay fixed.
How Failure Persistence Works
When Rendezvous detects a failure, it automatically saves the test configuration to:
.rendezvous-regressions/<contract-address>.<contract-name>.json
For example, failures in the counter contract deployed by ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM would be saved to:
.rendezvous-regressions/ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.counter.json
Regression File Format
The regression file stores failures grouped by test type (property-based tests vs invariant tests):
{
"invariant": [
{
"seed": 901717247,
"dial": "example/sip010.cjs",
"numRuns": 15,
"timestamp": 1767886534833
},
{
"seed": -1374686468,
"numRuns": 9,
"timestamp": 1767886531457
}
],
"test": [
{
"seed": 1656313995,
"numRuns": 6,
"timestamp": 1767886553125
},
{
"seed": 64830639,
"numRuns": 11,
"timestamp": 1767886546477
}
]
}
Each failure record includes:
seed– The random seed that triggered the failurenumRuns– Number of test iterations needed for the failure to occurtimestamp– When the failure was discovered (Unix timestamp in milliseconds)dial(optional) – Path to the dialer file used during the test
Failures are sorted by timestamp in descending order, with the most recent failures first.
Managing Regressions
To clear all saved regressions for a contract, simply delete its regression file:
rm .rendezvous-regressions/ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.counter.json
You can also manually edit the regression file to remove specific failures while keeping others.
6. Using a Config File
Instead of passing options as CLI flags, you can provide a JSON config file with --config. When a config file is used, all run options come from the file exclusively — CLI flags like --seed or --runs are ignored.
rv root contract test --config=rv.config.json
A config file is a JSON object with optional fields:
{
"accounts": [
{
"name": "whale_1",
"address": "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"
}
],
"accounts_mode": "overwrite",
"seed": 42,
"runs": 500,
"bail": true,
"dial": "./sip010.cjs"
}
| Field | Type | Description |
|---|---|---|
accounts | array of objects | Custom accounts (name and address fields required). |
accounts_mode | string | "overwrite" (default) or "concatenate". |
seed | integer | Seed for replay functionality. |
runs | positive integer | Number of test iterations. |
bail | boolean | Stop on first failure. |
regr | boolean | Run regression tests only. |
dial | string | Path to custom dialers file. |
The accounts field lets you define custom accounts for testing. By default ("overwrite" mode), these replace the Devnet.toml accounts entirely. With "concatenate" mode, config accounts are merged with the existing Devnet accounts — if a name appears in both, the config account’s address takes precedence.
Rendezvous warns if the config file contains unrecognized keys (e.g. a typo like "sedd" instead of "seed"), and also warns if CLI flags are passed alongside --config.
Summary
| Argument/Option | Description | Example |
|---|---|---|
<path-to-clarinet-project> | Path to the Clarinet project (where Clarinet.toml is located). | rv root contract test |
<contract-name> | Name of the contract to test (as in Clarinet.toml). | rv root contract test |
<type> | Type of test (test for property-based tests, invariant for invariant tests). | rv root contract test |
--runs=<num> | Sets the number of test iterations (default: 100). | rv root contract test --runs=500 |
--seed=<num> | Uses a specific seed for reproducibility. | rv root contract test --seed=12345 |
--regr | Run regression tests only (replay saved failures). | rv root contract test --regr |
--bail | Stop after the first failure. | rv root contract test --bail |
--dial=<file> | Loads JavaScript dialers from a file for pre/post-processing. | rv root contract test --dial=./custom-dialer.js |
--config=<file> | Uses a JSON config file for all run options. | rv root contract test --config=rv.config.json |
Understanding Rendezvous
Rendezvous makes property-based tests and invariant tests first-class. Tests are written in the same language as the system under test. This helps developers master the contract language. It also pushes boundaries—programmers shape their thoughts first, then express them using the language’s tools.
When Rendezvous initializes a Simnet session using a given Clarinet project, it deploys the contracts as defined in Clarinet.toml.
Example
Let’s say we have a contract named checker with the following source:
;; checker.clar
(define-public (check-it (flag bool))
(if flag (ok 1) (err u100))
)
;; #[env(simnet)]
(define-map context (string-ascii 100) {
called: uint
;; other data
}
)
;; #[env(simnet)]
(define-private (update-context (function-name (string-ascii 100)) (called uint))
(ok (map-set context function-name {called: called}))
)
;; #[env(simnet)]
(define-private (test-1)
(ok true)
)
;; #[env(simnet)]
(define-read-only (invariant-1)
true
)
The contract source, test functions, and context all live in the same file. The #[env(simnet)] annotation ensures the test functions are only deployed during simnet testing. Let’s take a closer look at the context.
The Rendezvous Context
Rendezvous uses a context to track function calls and execution details during invariant testing. This allows for better tracking of execution details and invariant validation.
Important: Every contract tested with Rendezvous invariant testing must include the
contextmap and theupdate-contextprivate function. During invariant testing, Rendezvous calls public functions and usesupdate-contextto track successful executions. This tracking enables invariants to reason about how many times each function has been called. If these are missing during invariant testing, Rendezvous will throw a runtime error. The context is not required for property-based testing.
How the Context Works
When a function is successfully executed during a test, Rendezvous records its execution details in a Clarity map. This map helps track how often specific functions are called successfully and can be extended for additional tracking in the future.
Here’s how the context is structured:
;; #[env(simnet)]
(define-map context (string-ascii 100) {
called: uint
;; Additional fields can be added here
})
;; #[env(simnet)]
(define-private (update-context (function-name (string-ascii 100)) (called uint))
(ok (map-set context function-name {called: called}))
)
Breaking it down
contextmap → Keeps track of execution data, storing how many times each function has been called successfully.update-contextfunction → Updates thecontextmap whenever a function executes, ensuring accurate tracking.
Using the context to write invariants
By tracking function calls, the context helps invariants ensure stronger correctness guarantees. For example, an invariant can verify that a counter stays above zero by checking the number of successful increment and decrement calls.
Example invariant using the context
(define-read-only (invariant-counter-gt-zero)
(let
(
(increment-num-calls
(default-to u0 (get called (map-get? context "increment")))
)
(decrement-num-calls
(default-to u0 (get called (map-get? context "decrement")))
)
)
(if
(<= increment-num-calls decrement-num-calls)
true
(> (var-get counter) u0)
)
)
)
By embedding execution tracking into the contract, Rendezvous enables more effective smart contract testing, making it easier to catch bugs and check the contract correctness.
Discarding Property-Based Tests
Rendezvous generates a wide range of inputs, but not all inputs are valid for every test. To skip tests with invalid inputs, there are two approaches:
Discard Function
A separate function determines whether a test should run.
Rules for a Discard Function:
- Must be read-only.
- Name must match the property test function, prefixed with
"can-".- Parameters must mirror the property test’s parameters.
- Must return
trueonly if inputs are valid, allowing the test to run.
Discard function example
(define-read-only (can-test-add (n uint))
(> n u1) ;; Only allow tests where n > 1
)
(define-private (test-add (n uint))
(let
((counter-before (get-counter)))
(try! (add n))
(asserts! (is-eq (get-counter) (+ counter-before n)) (err u403))
(ok true)
)
)
Here, can-test-add ensures that the test never executes for n <= 1.
In-Place Discarding
Instead of using a separate function, the test itself decides whether to run. If the inputs are invalid, the test returns (ok false), discarding itself.
In-place discarding example
(define-private (test-add (n uint))
(let
((counter-before (get-counter)))
(ok
(if
(<= n u1) ;; If n <= 1, discard the test.
false
(begin
(try! (add n))
(asserts! (is-eq (get-counter) (+ counter-before n)) (err u403))
true
)
)
)
)
)
In this case, if n <= 1, the test discards itself by returning (ok false), skipping execution.
Discarding summary
| Discard Mechanism | When to Use |
|---|---|
| Discard Function | When skipping execution before running the test is necessary. |
| In-Place Discarding | When discarding logic is simple and part of the test itself. |
In general, in-place discarding is preferred because it keeps test logic together and is easier to maintain. Use a discard function only when it’s important to prevent execution entirely.
Custom Manifest Files
Some smart contracts need a special Clarinet.toml file to allow Rendezvous to create state transitions in the contract. Rendezvous supports this feature by automatically searching for Clarinet-<target-contract-name>.toml first. This allows you to use test doubles while keeping tests easy to manage.
Why use a custom manifest?
A great example is the sBTC contract suite.
For testing the sbtc-token contract, the sbtc-registry authorization function is-protocol-caller is too restrictive. Normally, it only allows calls from protocol contracts, making it impossible to directly test certain state transitions in sbtc-token.
To work around this, you need two things:
A test double for sbtc-registry
You can create an sbtc-registry test double called sbtc-registry-double.clar:
;; contracts/sbtc-registry-double.clar
...
(define-constant deployer tx-sender)
;; Allows the deployer to act as a protocol contract for testing
(define-read-only (is-protocol-caller (contract-flag (buff 1)) (contract principal))
(begin
(asserts! (is-eq tx-sender deployer) (err u1234567)) ;; Enforces deployer check
(ok true)
)
)
...
This loosens the restriction just enough for testing by allowing the deployer to act as a protocol caller, while still enforcing an access check.
A Custom Manifest File
Next, create Clarinet-sbtc-token.toml to tell Rendezvous to use the test double only when targeting sbtc-token:
# Clarinet-sbtc-token.toml
...
[contracts.sbtc-registry]
path = 'contracts/sbtc-registry-double.clar'
clarity_version = 3
epoch = 3.0
...
How It Works
- When testing
sbtc-token, Rendezvous first checks ifClarinet-sbtc-token.tomlexists. - If found, it uses this file to initialize Simnet.
- If not, it falls back to the standard
Clarinet.toml.
This ensures that the test double is only used when testing sbtc-token, keeping tests realistic while allowing necessary state transitions.
Trait Reference Parameters
Rendezvous automatically generates arguments for function calls. It handles most Clarity types without any setup from you. However, trait references require special handling since Rendezvous cannot generate them automatically.
How Trait Reference Selection Works
When your functions accept trait reference parameters, you must include at least one trait implementation in your Clarinet project. This can be either a project contract or a requirement.
Here’s how Rendezvous handles trait references:
- Project Scanning – Before testing begins, Rendezvous scans your project for functions that use trait references.
- Implementation Discovery – It searches the contract AST for matching trait implementations and adds them to a selection pool.
- Random Selection – During test execution, Rendezvous randomly picks an implementation from the pool and uses it as a function argument.
This process allows Rendezvous to create meaningful state transitions and validate your invariants or property-based tests.
Example
The example Clarinet project demonstrates this feature. The send-tokens contract contains one public function and one property-based test that both accept trait references.
To enable testing, the project includes rendezvous-token, which implements the required trait.
Adding More Implementations
You can include multiple eligible trait implementations in your project. Adding more implementations allows Rendezvous to introduce greater randomness during testing and increases behavioral diversity. If a function that accepts a trait implementation parameter is called X times, those calls are distributed across the available implementations. As the number of implementations grows, Rendezvous has more options to choose from on each call, producing a wider range of behaviors — and uncovering edge cases that may be missed when relying on a single implementation.