-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Contract testing chapter and example
- Loading branch information
Showing
5 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[package] | ||
name = "tests" | ||
version = "0.1.0" | ||
|
||
[dependencies] | ||
starknet = ">=2.1.0-rc1" | ||
|
||
[[target.starknet-contract]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// ANCHOR: contract | ||
use starknet::ContractAddress; | ||
|
||
#[starknet::interface] | ||
trait ISimpleContract<TContractState> { | ||
fn get_value(self: @TContractState) -> u32; | ||
fn get_owner(self: @TContractState) -> ContractAddress; | ||
fn set_value(ref self: TContractState, value: u32); | ||
} | ||
|
||
#[starknet::contract] | ||
mod SimpleContract { | ||
use starknet::{get_caller_address, ContractAddress}; | ||
|
||
#[storage] | ||
struct Storage { | ||
value: u32, | ||
owner: ContractAddress | ||
} | ||
|
||
#[constructor] | ||
fn constructor(ref self: ContractState, initial_value: u32) { | ||
self.value.write(initial_value); | ||
self.owner.write(get_caller_address()); | ||
} | ||
|
||
#[external(v0)] | ||
impl SimpleContract of super::ISimpleContract<ContractState> { | ||
fn get_value(self: @ContractState) -> u32 { | ||
self.value.read() | ||
} | ||
|
||
fn get_owner(self: @ContractState) -> ContractAddress { | ||
self.owner.read() | ||
} | ||
|
||
fn set_value(ref self: ContractState, value: u32) { | ||
assert(self.owner.read() == get_caller_address(), 'Not owner'); | ||
self.value.write(value); | ||
} | ||
} | ||
} | ||
// ANCHOR_END: contract | ||
|
||
// ANCHOR: test | ||
#[cfg(test)] | ||
mod simple_contract_tests { | ||
// Import the interface and dispatcher to be able to interact with the contract. | ||
use super::{ | ||
ISimpleContract, SimpleContract, ISimpleContractDispatcher, ISimpleContractDispatcherTrait | ||
}; | ||
|
||
// Import the deploy syscall to be able to deploy the contract. | ||
use starknet::class_hash::Felt252TryIntoClassHash; | ||
use starknet::{ | ||
deploy_syscall, ContractAddress, get_caller_address, get_contract_address, | ||
contract_address_const | ||
}; | ||
|
||
// Use debug print trait to be able to print result if needed. | ||
use debug::PrintTrait; | ||
|
||
// Use starknet test utils to fake the transaction context. | ||
use starknet::testing::{set_caller_address, set_contract_address}; | ||
|
||
use serde::Serde; | ||
use option::OptionTrait; | ||
use array::ArrayTrait; | ||
use traits::{Into, TryInto}; | ||
use result::ResultTrait; | ||
|
||
// Deploy the contract and return its dispatcher. | ||
fn deploy(initial_value: u32) -> ISimpleContractDispatcher { | ||
// Set up constructor arguments. | ||
let mut calldata = ArrayTrait::new(); | ||
initial_value.serialize(ref calldata); | ||
|
||
// Declare and deploy | ||
let (contract_address, _) = deploy_syscall( | ||
SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false | ||
) | ||
.unwrap(); | ||
|
||
// Return the dispatcher. | ||
// The dispatcher allows to interact with the contract based on its interface. | ||
ISimpleContractDispatcher { contract_address } | ||
} | ||
|
||
#[test] | ||
#[available_gas(2000000000)] | ||
fn test_deploy() { | ||
let initial_value: u32 = 10; | ||
let contract = deploy(initial_value); | ||
|
||
assert(contract.get_value() == initial_value, 'wrong initial value'); | ||
assert(contract.get_owner() == get_contract_address(), 'wrong owner'); | ||
} | ||
|
||
#[test] | ||
#[available_gas(2000000000)] | ||
fn test_set_as_owner() { | ||
// Fake the caller address to address 1 | ||
let owner = contract_address_const::<1>(); | ||
set_caller_address(owner); | ||
|
||
let contract = deploy(10); | ||
assert(contract.get_owner() == owner, 'wrong owner'); | ||
|
||
// Fake the contract address to address 1 | ||
set_contract_address(owner); | ||
let new_value: u32 = 20; | ||
contract.set_value(new_value); | ||
|
||
assert(contract.get_value() == new_value, 'wrong value'); | ||
} | ||
|
||
#[test] | ||
#[should_panic] | ||
#[available_gas(2000000000)] | ||
fn test_set_not_owner() { | ||
let owner = contract_address_const::<1>(); | ||
set_caller_address(owner); | ||
|
||
let contract = deploy(10); | ||
|
||
let not_owner = contract_address_const::<2>(); | ||
set_contract_address(not_owner); | ||
|
||
let new_value: u32 = 20; | ||
contract.set_value(new_value); | ||
} | ||
} | ||
// ANCHOR_END: test | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Contract Testing | ||
|
||
Testing plays a crucial role in software development, especially for smart contracts. In this section, we'll guide you through the basics of testing a smart contract on Starknet with `scarb`. | ||
|
||
Let's start with a simple smart contract as an example: | ||
```rust | ||
{{#include ../listings/ch00-introduction/testing/src/lib.cairo:contract}} | ||
``` | ||
|
||
Now, take a look at the tests for this contract: | ||
```rust | ||
{{#include ../listings/ch00-introduction/testing/src/lib.cairo:test}} | ||
``` | ||
|
||
To define our test, we use scarb, which allows us to create a separate module guarded with `#[cfg(test)]`. This ensures that the test module is only compiled when running tests using `scarb test`. | ||
|
||
Each test is defined as a function with the `#[test]` attribute. You can also check if a test panics using the `#[should_panic]` attribute. | ||
|
||
As we are in the context of a smart contract, it's essential to set up the gas limit. You do this by using the `#[available_gas(X)]` attribute to specify the gas limit for a test. This is also a great way to ensure that your contract's features stay under a certain gas limit! | ||
|
||
Now, let's move on to the testing process: | ||
- Use the `deploy` function logic to declare and deploy your contract. | ||
- Use `assert` to verify that the contract behaves as expected in the given context. | ||
|
||
To make testing more convenient, the `testing` module of the corelib provides some helpful functions: | ||
- `set_caller_address(address: ContractAddress)` | ||
- `set_contract_address(address: ContractAddress)` | ||
- `set_block_number(block_number: u64)` | ||
- `set_block_timestamp(block_timestamp: u64)` | ||
- `set_account_contract_address(address: ContractAddress)` | ||
- `set_max_fee(fee: u128)` | ||
|
||
You may also need the `info` module from the corelib, which allows you to access information about the current transaction context: | ||
- `get_caller_address() -> ContractAddress` | ||
- `get_contract_address() -> ContractAddress` | ||
- `get_block_info() -> Box<BlockInfo>` | ||
- `get_tx_info() -> Box<TxInfo>` | ||
- `get_block_timestamp() -> u64` | ||
- `get_block_number() -> u64` | ||
|
||
|
||
You can found the full list of functions in the [Starknet Corelib repo](https://github.com/starkware-libs/cairo/tree/main/corelib/src/starknet). | ||
You can also find a detailled explaination of testing in cairo in the [Cairo book - Chapter 8](https://book.cairo-lang.org/ch08-01-how-to-write-tests.html). | ||
|
||
## Starket Foundry | ||
|
||
<!-- TODO update this when Starknet Foundry is more mature. --> | ||
|
||
Starknet Foundry is a powerful toolkit for developing smart contracts on Starknet. It offers support for testing Starknet smart contracts on top of `scarb` with the `Forge` tool. | ||
|
||
Testing with `snforge` is similar to the process we just described but simplified. Moreover, additional features are on the way, including cheatcodes or parallel tests execution. We highly recommend exploring Starknet Foundry and incorporating it into your projects. | ||
|
||
For more detailed information about testing contracts with Starknet Foundry, check out the [Starknet Foundry Book - Testing Contracts](https://foundry-rs.github.io/starknet-foundry/testing/contracts.html). |