Skip to content

Commit

Permalink
Merge pull request #1418 from sczembor/testing_smart_contracts
Browse files Browse the repository at this point in the history
Testing smart contracts
  • Loading branch information
sczembor authored Apr 16, 2024
2 parents 7caad27 + b094810 commit a90f97d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ The Casper test support crate is one of many options for testing contracts befor

:::


### Defining Dependencies in `Cargo.toml`

This guide uses the project structure, and example contract outlined [here](./simple-contract.md#directory-structure) for creating tests.
Expand All @@ -19,14 +18,14 @@ To begin, outline the required test dependencies in the `/tests/Cargo.toml` file

```rust
[dependencies]
casper-execution-engine = "2.0.1"
casper-engine-test-support = { version = "2.2.0", features = ["test-support"] }
casper-types = "1.5.0"
casper-execution-engine = "5.0.0"
casper-engine-test-support = { version = "5.0.0", features = ["test-support"] }
casper-types = "3.0.0"
```

- `casper-execution-engine` - This crate imports the execution engine functionality, enabling Wasm execution within the test framework. Each node contains an instance of an execution engine, and the testing framework simulates this behavior.
- `casper-engine-test-support` - A helper crate that provides the interface to write tests and interact with an instance of the execution engine.
- `casper-types` - Types shared by many Casper crates for use on a Casper network.
- `casper-execution-engine` - This crate imports the execution engine functionality, enabling Wasm execution within the test framework. Each node contains an instance of an execution engine, and the testing framework simulates this behavior.
- `casper-engine-test-support` - A helper crate that provides the interface to write tests and interact with an instance of the execution engine.
- `casper-types` - Types shared by many Casper crates for use on a Casper network.

## Writing the Tests {#writing-the-tests}

Expand Down Expand Up @@ -55,7 +54,7 @@ Import external test support, which includes a variety of default values and hel
// Outlining aspects of the Casper test support crate to include.
use casper_engine_test_support::{
ExecuteRequestBuilder, InMemoryWasmTestBuilder, DEFAULT_ACCOUNT_ADDR,
DEFAULT_RUN_GENESIS_REQUEST,
PRODUCTION_RUN_GENESIS_REQUEST,
};
// Custom Casper types that will be used within this test.
use casper_types::{runtime_args, ContractHash, RuntimeArgs};
Expand All @@ -64,30 +63,36 @@ Import external test support, which includes a variety of default values and hel
Next, you need to define any global variables or constants for the test.

```rust
const COUNTER_V1_WASM: &str = "counter-v1.wasm"; // The first version of the contract
const COUNTER_V2_WASM: &str = "counter-v2.wasm"; // The second version of the contract
const COUNTER_CALL_WASM: &str = "counter-call.wasm"; // Session code that calls the contract

const CONTRACT_KEY: &str = "counter"; // Named key referencing this contract
const COUNT_KEY: &str = "count"; // Named key referencing the value to increment/decrement
const CONTRACT_VERSION_KEY: &str = "version"; // Key maintaining the version of a contract package

const ENTRY_POINT_COUNTER_DECREMENT: &str = "counter_decrement"; // Entry point to decrement the count value
const ENTRY_POINT_COUNTER_INC: &str = "counter_inc"; // Entry point to increment the count value
// Contract Wasm File Paths (Constants)
const COUNTER_V1_WASM: &str = "counter-v1.wasm";
const COUNTER_V2_WASM: &str = "counter-v2.wasm";
const COUNTER_V3_WASM: &str = "counter-v3.wasm";
const COUNTER_CALL_WASM: &str = "counter-call.wasm";

// Contract Storage Keys (Constants)
const CONTRACT_KEY: &str = "counter";
const COUNT_KEY: &str = "count";
const LAST_UPDATED_KEY: &str = "last_updated";
const CONTRACT_VERSION_KEY: &str = "version";

// Contract Entry Points (Constants)
const ENTRY_POINT_COUNTER_DECREMENT: &str = "counter_decrement";
const ENTRY_POINT_COUNTER_INC: &str = "counter_inc";
const ENTRY_POINT_COUNTER_LAST_UPDATED_AT: &str = "counter_last_updated_at";
```

### Creating a Test Function

Each test function installs the contract and calls entry points to assert that the contract's behavior matches expectations. The test uses the `InMemoryWasmTestBuilder` to invoke an instance of the execution engine, effectively simulating the process of installing the contract on the chain.

As part of this process, we use the `DEFAULT_RUN_GENESIS_REQUEST` to install the system contracts necessary for the tests, including the `Mint`, `Auction`, and `HandlePayment`contracts, as well as establishing a default account and funding the associated purse.
As part of this process, we use the `PRODUCTION_RUN_GENESIS_REQUEST` to install the system contracts necessary for the tests, including the `Mint`, `Auction`, and `HandlePayment`contracts, as well as establishing a default account and funding the associated purse.

```rust
#[test]
/// Install version 1 of the counter contract and check its available entry points. ...
fn install_version1_and_check_entry_points() {
let mut builder = InMemoryWasmTestBuilder::default();
builder.run_genesis(&*DEFAULT_RUN_GENESIS_REQUEST).commit();
builder.run_genesis(&PRODUCTION_RUN_GENESIS_REQUEST).commit();

// See the repository for the full function.
}
Expand Down Expand Up @@ -171,6 +176,33 @@ The following session code uses the contract hash to identify the contract, the
.commit();
```

:::tip

**Testing Time-Sensitive Functions**

Normally, smart contracts operate on a blockchain where time advances in blocks. Testing functions that rely on time can be tricky.

**Simulating Time with `with_block_time`**

When building a request to call a contract function (using `ExecuteRequestBuilder`), you can set a custom block time with `.with_block_time(desired_time)`. This pretends the function is called at that specific time.

**Example:**

```rust
let session_code_request = ExecuteRequestBuilder::standard(
*DEFAULT_ACCOUNT_ADDR,
COUNTER_CALL_WASM,
runtime_args! {
CONTRACT_KEY => contract_v1_hash
},
.with_block_time(5000)
.build();
```

This lets you test how your contract behaves at different points in time, all within your unit test.

:::

#### Evaluating and Comparing Results

After calling the contract, we should verify the results received to ensure the contract operated as intended. The `builder` method retrieves the required information and converts it to the value type required. Then, `assert_eq!()` compares the result against the expected value.
Expand Down Expand Up @@ -199,10 +231,10 @@ Each contract installation will require an additional Wasm file installed throug

The major difference between calling a contract from session code versus contract code is the ability to use non-standard dependencies for the `ExecuteRequestBuilder`. Where session code must designate a Wasm file within the standard dependencies, contract code can use one of the four available options for calling other contracts, namely:

- `contract_call_by_hash` - Calling a contract by its `ContractHash`.
- `contract_call_by_name` - Calling a contract referenced by a named key in the signer's Account context.
- `versioned_contract_call_by_hash` - Calling a specific contract version using its `ContractHash`.
- `versioned_contract_call_by_name` - Calling a specific version of a contract referenced by a named key in the signer's Account context.
- `contract_call_by_hash` - Calling a contract by its `ContractHash`.
- `contract_call_by_name` - Calling a contract referenced by a named key in the signer's Account context.
- `versioned_contract_call_by_hash` - Calling a specific contract version using its `ContractHash`.
- `versioned_contract_call_by_name` - Calling a specific version of a contract referenced by a named key in the signer's Account context.

The calling contract must also provide an entry point and any necessary runtime arguments in all cases.

Expand Down Expand Up @@ -241,5 +273,5 @@ You may also wish to test your contracts on the Casper [Testnet](https://testnet

## What's Next? {#whats-next}

- Understand [session code](./contract-vs-session.md#what-is-session-code) and how it triggers a smart contract.
- Learn to [install a contract and query global state](../cli/installing-contracts.md) with the Casper command-line client.
- Understand [session code](./contract-vs-session.md#what-is-session-code) and how it triggers a smart contract.
- Learn to [install a contract and query global state](../cli/installing-contracts.md) with the Casper command-line client.
37 changes: 22 additions & 15 deletions source/docs/casper/resources/beginner/counter/walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@ First, you will need to clone [the counter contract repository](https://github.c
git clone https://github.com/casper-ecosystem/counter
```

If you explore the source code, you will see that there are two versions of the counter contract and one file with session code that calls the contract's entry-points:
If you explore the source code, you will see that there are three versions of the counter contract and one file with session code that calls the contract's entry-points:

- `contract-v1`
- **`contract-v1`**

- This is the first version of the counter contract.
- Defines two named keys: _counter_ to reference the contract and an associated variable _count_ to store a value.
- Provides a function to get the current count (_count_get_).
- Provides a function to increment the current count (_counter_inc_).

- `contract-v2`
- This is the first version of the counter contract.
- It defines two named keys:
- `counter`: References the contract itself.
- `count`: Stores the current counter value.
- It provides functions for:
- `get_count`: Retrieves the current counter value.
- `counter_inc`: Increments the counter value by 1.

- This is a second version of the counter contract, which will not be used in this tutorial.
- This version provides an additional function to decrement the counter and to demonstrate contract upgrades in another tutorial.
- **`contract-v2`** (Not Used in This Tutorial)

- `counter-call`
- An extension of `contract-v1`. It demonstrates decrementing the counter and contract upgrades.

- This is session code that retrieves the _contract-v1_ contract, gets the current count value, increments it, and ensures the count was incremented by 1.
- **`contract-v3` ** (Not Used in This Tutorial)

- This version showcases how to add new functionalities during smart contract upgrades. It extends `contract-v1` and `contract-v2` by introducing:
- A new named key: `last_updated_at` - Tracks the timestamp of the last counter update.
- A new entry point: `get_last_updated_at` - Retrieves the `last_updated_at` timestamp.
- It focuses on the process of adding new fields like `last_updated_at` to existing contracts.

- **`counter-call`**
- Session code that retrieves the specific contract version (e.g., `contract-v1`), interacts with its functions (e.g., `get_count`, `counter_inc`), and verifies the expected behavior.

## Create a Local Network {#create-a-local-network}

Expand Down Expand Up @@ -68,7 +76,7 @@ Get the state root hash:
casper-client get-state-root-hash --node-address http://localhost:11101
```

You are using localhost as the node server since the network is running on our local machine. Make a note of the _state-root-hash_ that is returned, but keep in mind that this hash value will need to be updated every time you modify the network state.
You are using localhost as the node server since the network is running on our local machine. Make a note of the _state-root-hash_ that is returned, but keep in mind that this hash value will need to be updated every time you modify the network state.

Finally, query the actual state:

Expand Down Expand Up @@ -223,7 +231,6 @@ casper-client put-deploy \

To make sure that the session code ran successfully, get the new state root hash and query the network.


```bash
casper-client get-state-root-hash --node-address http://localhost:11101
```
Expand All @@ -236,6 +243,6 @@ casper-client query-global-state --node-address http://localhost:11101 \
--key [ACCOUNT_HASH] -q "counter/count"
```

If all went according to plan, your count value should be 2 at this point.
If all went according to plan, your count value should be 2 at this point.

Congratulations on building, installing, and using a smart contract on your local network!
Loading

0 comments on commit a90f97d

Please sign in to comment.