智能合同测试
在本节中,我们将使用CasperLabs引擎测试支持工具箱针对执行环境测试ERC-20智能合约,该执行环境等效于CasperLabs在生产中使用的环境。在这里,我们将创建2个文件,这些文件将为ERC20合同建立测试框架。
以下是完成测试的示例。
#[test]
fn test_erc20_transfer() {
let amount = 10;
let mut token = ERC20Contract::deployed();
token.transfer(BOB, amount, Sender(ALI));
assert_eq!(token.balance_of(ALI), ERC20_INIT_BALANCE - amount);
assert_eq!(token.balance_of(BOB), amount);
}
删除tests/src/integration_tests.rs并创建三个文件:
tests/src/erc20.rs -设置测试上下文并创建单元测试使用的辅助函数
tests/src/tests.rs -包含单元测试
tests/src/lib.rs-防锈工具链要求。将其他2个文件链接在一起
该tests箱具有一个build.rs文件:有效的自定义生成脚本。它在每次运行测试之前都会执行一次,并且contract为了您的方便起见,它将在发布模式下编译条板箱,并将contract.wasm文件复制到tests/wasm目录中。实际上,这意味着我们只需要在开发过程中运行即可。cargo test -p tests
设置Cargo.toml
在定义一个tests包tests/Cargo.toml。
[package]
name = "tests"
version = "0.1.1"
authors = ["Your Name here
edition = "2020"
[dependencies]
casperlabs-contract = "0.6.1"
casperlabs-types = "0.6.1"
casperlabs-engine-test-support = "0.8.1"
[features]
default = ["casperlabs-contract/std", "casperlabs-types/std"]
创建ERC20.rs逻辑进行测试
建立了测试上下文
首先定义将在测试之间重用的常量,例如方法名,键名和帐户地址。这将使用智能合约正确运行所需的所有数据和方法来初始化全局状态。
// tests/src/erc20.rs
pub mod account {
use super::PublicKey;
pub const ALI: PublicKey = PublicKey::ed25519_from([1u8; 32]);
pub const BOB: PublicKey = PublicKey::ed25519_from([2u8; 32]);
pub const JOE: PublicKey = PublicKey::ed25519_from([3u8; 32]);
}
pub mod token_cfg {
use super::*;
pub const NAME: &str = "ERC20";
pub const SYMBOL: &str = "STX";
pub const DECIMALS: u8 = 18;
pub fn total_supply() -> U256 { 1_000.into() }
}
pub struct Sender(pub AccountHash);
pub struct Token {
context: TestContext
}
部署合同
下一步是定义ERC20Contract具有自己的VM实例并实现ERC-20方法的结构。该结构应具有TestContext其自己的a。部署合同后,令牌合同哈希和erc20_indirect会话代码哈希将保持不变,因此很方便使用它。此代码段构建了上下文,并包括contract.wasm正在测试的已编译二进制文件。这个函数创建的新实例ERC20Contract有ALI,BOB和JOE具有正初始余额。使用该ALI帐户部署合同。
// tests/src/erc20.rs
// the contract struct
pub struct Token {
context: TestContext
}
impl Token {
pub fn deployed() -> Token {
// Builds test context with Alice & Bob's accounts
let mut context = TestContextBuilder::new()
.with_account(account::ALI, U512::from(128_000_000))
.with_account(account::BOB, U512::from(128_000_000))
.build();
// Adds compiled contract to the context with arguments specified above.
// For this example it is 'ERC20' & 'STX'
let session_code = Code::from("contract.wasm");
let session_args = runtime_args! {
"tokenName" => token_cfg::NAME,
"tokenSymbol" => token_cfg::SYMBOL,
"tokenTotalSupply" => token_cfg::total_supply()
};
// Builds the session with the code and arguments
let session = SessionBuilder::new(session_code, session_args)
.with_address(account::ALI)
.with_authorization_keys(&[account::ALI])
.build();
//Runs the code
context.run(session);
Token { context }
}
查询系统
上面的步骤模拟了网络上的实际部署。此代码段描述了如何查询合同的哈希值。合同是在帐户环境下部署的。由于部署是在的上下文下创建的account::ALI,因此需要在此处查询。该query_contract函数用于query查找命名键。它将被用来实现balance_of, total_supply和allowance检查。
fn contract_hash(&self) -> Hash {
self.context
.query(account::ALI, &[format!("{}_hash", token_cfg::NAME)])
.unwrap_or_else(|_| panic!("{} contract not found", token_cfg::NAME))
.into_t()
.unwrap_or_else(|_| panic!("{} has wrong type", token_cfg::NAME))
}
// This function is a generic helper function that queries for a named key defined in the contract.
fn query_contract
match self.context.query(
account::ALI,
&[token_cfg::NAME, &name.to_string()],
) {
Err(_) => None,
Ok(maybe_value) => {
let value = maybe_value
.into_t()
.unwrap_or_else(|_| panic!("{} is not expected type.", name));
Some(value)
}
}
}
// Here we call the helper function to query on specific named keys defined in the contract.
// Returns the name of the token
pub fn name(&self) -> String {
self.query_contract("_name").unwrap()
}
// Returns the token symbol
pub fn symbol(&self) -> String {
self.query_contract("_symbol").unwrap()
}
// Returns the number of decimal places for the token
pub fn decimals(&self) -> u8 {
self.query_contract("_decimals").unwrap()
}
调用在合同法
此代码段描述了调用合同中特定入口点的通用方法。
fn call(&mut self, sender: Sender, method: &str, args: RuntimeArgs) {
let Sender(address) = sender;
let code = Code::Hash(self.contract_hash(), method.to_string());
let session = SessionBuilder::new(code, args)
.with_address(address)
.with_authorization_keys(&[address])
.build();
self.context.run(session);
}
调用合同中的每种getter方法。
pub fn balance_of(&self, account: AccountHash) -> U256 {
let key = format!("_balances_{}", account);
self.query_contract(&key).unwrap_or_default()
}
pub fn allowance(&self, owner: AccountHash, spender: AccountHash) -> U256 {
let key = format!("_allowances_{}_{}", owner, spender);
self.query_contract(&key).unwrap_or_default()
}
pub fn transfer(&mut self, recipient: AccountHash, amount: U256, sender: Sender) {
self.call(sender, "transfer", runtime_args! {
"recipient" => recipient,
"amount" => amount
});
}
pub fn approve(&mut self, spender: AccountHash, amount: U256, sender: Sender) {
self.call(sender, "approve", runtime_args! {
"spender" => spender,
"amount" => amount
});
}
pub fn transfer_from(&mut self, owner: AccountHash, recipient: AccountHash, amount: U256, sender: Sender) {
self.call(sender, "transferFrom", runtime_args! {
"owner" => owner,
"recipient" => recipient,
"amount" => amount
});
}
}
创建带有单位的tests.rs文件
单元测试
现在我们有了一个测试上下文,我们可以使用该上下文并通过调用中定义的功能来创建用于测试合同代码的单元测试 tests/src/erc20.rs。将这些功能添加到中tests/src/tests.rs。
// tests/src/tests.rs
use crate::erc20::{Token, Sender, account::{ALI, BOB, JOE}, token_cfg};
#[test]
fn test_erc20_deploy() {
let token = Token::deployed();
assert_eq!(token.name(), token_cfg::NAME);
assert_eq!(token.symbol(), token_cfg::SYMBOL);
assert_eq!(token.decimals(), token_cfg::DECIMALS);
assert_eq!(token.balance_of(ALI), token_cfg::total_supply());
assert_eq!(token.balance_of(BOB), 0.into());
assert_eq!(token.allowance(ALI, ALI), 0.into());
assert_eq!(token.allowance(ALI, BOB), 0.into());
assert_eq!(token.allowance(BOB, ALI), 0.into());
assert_eq!(token.allowance(BOB, BOB), 0.into());
}
#[test]
fn test_erc20_transfer() {
let amount = 10.into();
let mut token = Token::deployed();
token.transfer(BOB, amount, Sender(ALI));
assert_eq!(token.balance_of(ALI), token_cfg::total_supply() - amount);
assert_eq!(token.balance_of(BOB), amount);
}
#[test]
#[should_panic]
fn test_erc20_transfer_too_much() {
let amount = 1.into();
let mut token = Token::deployed();
token.transfer(ALI, amount, Sender(BOB));
}
#[test]
fn test_erc20_approve() {
let amount = 10.into();
let mut token = Token::deployed();
token.approve(BOB, amount, Sender(ALI));
assert_eq!(token.balance_of(ALI), token_cfg::total_supply());
assert_eq!(token.balance_of(BOB), 0.into());
assert_eq!(token.allowance(ALI, BOB), amount);
assert_eq!(token.allowance(BOB, ALI), 0.into());
}
#[test]
fn test_erc20_transfer_from() {
let allowance = 10.into();
let amount = 3.into();
let mut token = Token::deployed();
token.approve(BOB, allowance, Sender(ALI));
token.transfer_from(ALI, JOE, amount, Sender(BOB));
assert_eq!(token.balance_of(ALI), token_cfg::total_supply() - amount);
assert_eq!(token.balance_of(BOB), 0.into());
assert_eq!(token.balance_of(JOE), amount);
assert_eq!(token.allowance(ALI, BOB), allowance - amount);
}
#[test]
#[should_panic]
fn test_erc20_transfer_from_too_much() {
let amount = token_cfg::total_supply().checked_add(1.into()).unwrap();
let mut token = Token::deployed();
token.transfer_from(ALI, JOE, amount, Sender(BOB));
}
配置lib.rs以通过cargo运行所有内容
在tests/src/lib.rs文件中,添加以下行。这告诉货物在运行测试时要使用哪些文件。
#[cfg(test)]
pub mod tests;
#[cfg(test)]
pub mod erc20;
运行测试!
运行测试以验证它们是否有效。这是通过运行的bash。
$ cargo test -p tests
作者郑重申明:截至发文时,作者与文中提及项目皆不存在任何利益关系。