CosmWasm MultiTest 2.1.0 is out!
Recently, version 2.1.0 of CosmWasm MultiTest has been released! In the following sections, we explain in detail what has changed compared to the previous version.
Summary of major changes in CosmWasm MultiTest 2.1.0
- removed all default feature flags,
- removed all feature flags from cosmwasm-std dependency,
- added
staking
,stargate
andcosmwasm_x_y
feature flags, - made function
addr_make
available during initialization, - removed validation of validator addresses,
- enabled empty string attributes,
- improved the unbonding.
🔩 Refactored feature flags
We have recognized the importance of giving users greater control over their testing environments in terms of dependency configuration. We decided — although it’s a breaking change (in the SemVer sense) — to remove all default features from MultiTest. Starting with version 2.1.0 of MultiTest, we will strive to keep the default features set empty.
As a consequence, users will have to specify precisely which CosmWasm version they need for simulating their chain in tests. Generally — since version 2.0.0 — MultiTest is designed for testing smart contracts based on CosmWasm 2.0 or later.
Specifying the following features enables all testing capabilities for smart contracts based on CosmWasm 2.0 or later:
[dev-dependencies]
cw-multi-test = { version = "2.1.0", features = ["cosmwasm_2_0", "staking", "stargate"] }
Another step towards flexibility was removing features from cosmwasm-std dependency. Now, cosmwasm-std/staking
and cosmwasm-std/stargate
features can be enabled using MultiTest features staking
and stargate
, respectively. The full list of MultiTest features since version 2.1.0 is shown below.
Summary of features in CosmWasm MultiTest 2.1.0
backtrace
— enablesbacktrace
feature in anyhow dependency,staking
— enablesstaking
feature in cosmwasm-std dependency and enables staking module in MultiTest,stargate
— enablesstargate
feature in cosmwasm-std dependency and enables stargate module in MultiTest,cosmwasm_1_1
— enablescosmwasm_1_1
feature in cosmwasm-std dependency,cosmwasm_1_2
— enablescosmwasm_1_2
feature in cosmwasm-std dependency andcosmwasm_1_1
feature in MultiTest,cosmwasm_1_3
— enablesosmwasm_1_3
feature in cosmwasm-std dependency andcosmwasm_1_2
feature in MultiTest,cosmwasm_1_4
— enablescosmwasm_1_4
feature in cosmwasm-std dependency andcosmwasm_1_3
feature in MultiTest,cosmwasm_2_0
— enablescosmwasm_2_0
feature in cosmwasm-std dependency andcosmwasm_1_4
feature in MultiTest.
🔧 Enabled Bech32 addresses during
initialization
In tests, during chain initialization, it’s a common case to fund some tokens to user addresses, that are later used for subsequent bank operations. An example of such test case is shown below. Please note, that a single user address is created using app.api().addr_make
function after chain creation but before chain initialization.
use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::App;
const USER: &str = "alice";
let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};
// create a chain
let mut app = App::default();
// create single user address used in subsequent operations
let user_addr = app.api().addr_make(USER);
// fund some tokens to user address during chain initialization
app.init_modules(|router, _, storage| {
router
.bank
.init_balance(storage, &user_addr, vec![coin])
.unwrap();
});
// use user address to query it's balances
let balances = app.wrap().query_all_balances(user_addr).unwrap();
assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());
The above use case was straightforward, but what if we want to create and initialize the chain in single step? It’s now possible to use the function addr_make
which is available in api
argument passed to initialization closure. The difference from the previous example is that now we call addr_make
function twice: once during chain initialization and once for querying the balances, but the resulting user address is the same.
use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::App;
const USER: &str = "alice";
let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};
// create and initialize the chain in one operation
let app = App::new(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});
// query user's balances
let balances = app
.wrap()
.query_all_balances(app.api().addr_make(USER))
.unwrap();
assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());
Another more sophisticated example is shown below. The chain is created and initialized in a single step using AppBuilder
, setting up mock Api
for generating Bech32m addresses. Like in the previous example, inside the initialization closure, function add_make
is used for user address generation. The resulting address will be in Bech32m format with juno prefix.
use cosmwasm_std::{Coin, Uint128};
use cw_multi_test::AppBuilder;
use cw_multi_test::MockApiBech32m;
const USER: &str = "alice";
let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};
// create and initialize the chain in one operation
// use Bech32m format for user addresses
// use "juno" prefix for the chain
let app = AppBuilder::new()
.with_api(MockApiBech32m::new("juno"))
.build(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});
let user_addr = app.api().addr_make(USER);
// just to see how the user address looks like
assert_eq!(
"juno190vqdjtlpcq27xslcveglfmr4ynfwg7gmw86cnun4acakxrdd6gqg3ddu4",
user_addr.as_str()
);
// query user's balances
let balances = app.wrap().query_all_balances(user_addr).unwrap();
assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());
The last example shows the usage of custom_app
function with initialization closure that uses addr_make
function to generate a user address.
use cosmwasm_std::{Coin, CustomMsg, CustomQuery, Uint128};
use cw_multi_test::{custom_app, BasicApp};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
const USER: &str = "alice";
let coin = Coin {
denom: "LUNC".to_string(),
amount: Uint128::new(10),
};
// custom message used in simulated chain
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename = "snake_case")]
pub enum ExampleMsg {
MsgEx,
}
impl CustomMsg for ExampleMsg {}
// custom query used in simulated chain
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename = "snake_case")]
pub enum ExampleQuery {
QueryEx,
}
impl CustomQuery for ExampleQuery {}
// create and initialize a chain with cutom message and custom query
let app: BasicApp<ExampleMsg, ExampleQuery> = custom_app(|router, api, storage| {
router
.bank
.init_balance(storage, &api.addr_make(USER), vec![coin])
.unwrap();
});
// query user's balances
let balances = app
.wrap()
.query_all_balances(app.api().addr_make(USER))
.unwrap();
assert_eq!("LUNC", balances[0].denom);
assert_eq!(10, balances[0].amount.u128());
As a supplement, the usage of addr_make
function in different contexts is shown in the following section.
Summary of address generation options in tests
For convenience, there are multiple options available for generating user addresses in tests. The right choice depends on the specific test case. The most popular usages are depicted in the following examples. Please note, that addresses are the same when prefix and format are the same, no matter which method was used to generate it.
Examples using MockApi
from cosmwasm-std::testing module
use cosmwasm_std::testing::MockApi;
let addr = MockApi::default().addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cosmwasm_std::testing::MockApi;
let addr = MockApi::default().with_prefix("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);
Examples using MockApiBech32
from cw_multi_test
use cw_multi_test::MockApiBech32;
let addr = MockApiBech32::new("cosmwasm").addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::MockApiBech32;
let addr = MockApiBech32::new("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);
Examples using MockApiBech32m
from cw_multi_test
use cw_multi_test::MockApiBech32m;
let addr = MockApiBech32m::new("cosmwasm").addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsl5lc5x",
addr.as_str()
);
use cw_multi_test::MockApiBech32m;
let addr = MockApiBech32m::new("juno").addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsw56lgx",
addr.as_str()
);
Examples using IntoAddr
from cw_multi_test
use cw_multi_test::IntoAddr;
let addr = "owner".into_addr();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::IntoAddr;
let addr = "owner".into_addr_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);
Examples using IntoBech32
from cw_multi_test
use cw_multi_test::IntoBech32;
let addr = "owner".into_bech32();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::IntoBech32;
let addr = "owner".into_bech32_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);
Examples using IntoBech32m
from cw_multi_test
use cw_multi_test::IntoBech32m;
let addr = "owner".into_bech32m();
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsl5lc5x",
addr.as_str()
);
use cw_multi_test::IntoBech32m;
let addr = "owner".into_bech32m_with_prefix("juno");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsw56lgx",
addr.as_str()
);
Examples using App
from cw_multi_test
use cw_multi_test::App;
let app = App::default();
let addr = app.api().addr_make("owner");
assert_eq!(
"cosmwasm1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqs2g053y",
addr.as_str()
);
use cw_multi_test::{no_init, AppBuilder};
let app = AppBuilder::default()
.with_api(MockApiBech32::new("juno"))
.build(no_init);
let addr = app.api().addr_make("owner");
assert_eq!(
"juno1fsgzj6t7udv8zhf6zj32mkqhcjcpv52yph5qsdcl0qt94jgdckqsmg2ndy",
addr.as_str()
);
🪚 Removed validation of validator addresses
Before the version 2.1.0 of MultiTest, the default implementation of the staking module was checking validators’ addresses against Bech32 format with the same prefix as defined for the chain. While developers were using addr_make
function to create validator address used later in tests, everything worked fine. In real-life blockchains, the validator (operator) address has a different prefix when converted to a human-readable format compared to the chain’s prefix. This difference was the reason for mysterious errors indicating an invalid Bech32 format. Since version 2.1.0 of MultiTest, this issue is fixed.
A working example is given in the last code snippet in this article, just compare the following code fragments:
// Validator address, note this has a different prefix that the chain.
let validator_addr = "validator".into_bech32_with_prefix("testvaloper");
// Delegator address has chain's prefix.
let delegator_addr = app.api().addr_make(DELEGATOR);
⚙️ Enabled empty string attributes in tests
In wasmd, since version 0.45.0, it has been allowed to return empty string values in Response
attributes. This was not the case in MultiTest until version 2.1.0. Now, in tests, contract developers may use empty string values in attributes, either in Response
or in Event
. Below is an example of a contract returning empty string attributes.
use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env,
Event, MessageInfo, Response, StdError};
use cw_multi_test::{Contract, ContractWrapper};
fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> Result<Response, StdError> {
Ok(Response::default())
}
fn execute(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: Empty,
) -> Result<Response, StdError> {
Ok(Response::<Empty>::new()
.add_attribute("city", " ") // <-- empty string
.add_attribute("street", "") // <-- empty string
.add_event(
Event::new("location")
.add_attribute("longitude", " ") // <-- empty string
.add_attribute("latitude", ""), // <-- empty string
))
}
fn query(_deps: Deps, _env: Env, _msg: Empty) -> Result<Binary, StdError> {
Ok(Binary::default())
}
fn contract() -> Box<dyn Contract<Empty>> {
Box::new(ContractWrapper::new_with_empty(execute, instantiate, query))
}
#[test]
fn empty_string_attribute_should_work() {
use cw_multi_test::{App, Executor};
// prepare the blockchain
let mut app = App::default();
// prepare sender address
let sender = app.api().addr_make("sender");
// store the contract's code
let code_id = app.store_code(contract());
// instantiate the contract
let contract_addr = app
.instantiate_contract(
code_id,
sender.clone(),
&Empty{},
&[],
"attributed",
None
).unwrap();
// execute message on the contract, this returns response
// with attributes having empty string values
assert!(app
.execute_contract(sender, contract_addr, &Empty{}, &[])
.is_ok());
}
🗜️ Improved the unbonding
When users stake or bond their tokens to participate in network activities like securing the blockchain, earning rewards, or voting in governance, they often have to wait for a specified period before they can withdraw their staked assets. This period is called the unbonding time. In MultiTest, the check to see if the unbonding time has elapsed is done against the current block time. There is a dedicated function named update_block
in the App
to advance the block’s height and time. An example how to advance the block is shown below.
use cw_multi_test::App;
let mut app = App::default();
// let's check the default block height and time
let default_height = app.block_info().height;
let default_time = app.block_info().time.nanos();
assert_eq!(12345, default_height);
// timestamp of value 1571797419879305533 represents
// Wed Oct 23 2019 02:23:39 GMT+0000
assert_eq!(1571797419879305533, default_time);
// let's advance the block height with 1,
// and block time with 1 minute
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_minutes(1);
});
// let's retrieve the new block's height and time
let new_height = app.block_info().height;
let new_time = app.block_info().time.nanos();
// the block height should be advances with 1
assert_eq!(new_height, default_height + 1);
// the block time should be advances with 1 minute
// 1 minute = 60 s = 60_000_000_000 ns
assert_eq!(new_time, default_time + 60_000_000_000);
// the new height is:
assert_eq!(12346, new_height);
// the new timestamp of value 1571797479879305533 represents
// Wed Oct 23 2019 02:24:39 GMT+0000
// so 1 minute later than the default value
assert_eq!(1571797479879305533, new_time);
Please note that in MultiTest, it is possible to advance only one attribute of the block: either height or time. But this is NOT a normal situation in a real-life blockchain, so ALWAYS keep in mind to advance both the block height and block time in tests.
In previous versions of MultiTest there was a very specific situation when the unbonding time was not properly checked, even after advancing both the height and the time of the block, and this issue was fixed in version 2.1.0 of MultiTest.
The example test case with bonding and unbonding process is shown in the following code snippet. To compile this code, please make sure the staking
feature is added to cw-multi-test dependency.
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Decimal, StakingMsg, Validator};
use cw_multi_test::{AppBuilder, Executor, IntoBech32, StakingInfo};
/// Denominator of the staking token.
const BONDED_DENOM: &str = "stake";
// Time between unbonding and receiving tokens back (in seconds).
const UNBONDING_TIME: u64 = 60;
// Amount of tokens to be (re)delegated
const DELEGATION_AMOUNT: u128 = 100;
// Initial amount of tokens for delegator.
const INITIAL_AMOUNT: u128 = 1000;
// Amount of tokens after delegation.
const FEWER_AMOUNT: u128 = INITIAL_AMOUNT - DELEGATION_AMOUNT;
// Name of the user thet delegates staking.
const DELEGATOR: &str = "delegator";
// Validator address, note this has a different prefix that the chain.
let validator_addr = "validator".into_bech32_with_prefix("testvaloper");
let valoper = Validator::new(
validator_addr.to_string(),
Decimal::percent(10),
Decimal::percent(90),
Decimal::percent(1),
);
// prepare the blockchain configuration
let block = mock_env().block;
let mut app = AppBuilder::default().build(|router, api, storage| {
// set initial balance for the delegator
router
.bank
.init_balance(
storage,
&api.addr_make(DELEGATOR),
vec![coin(INITIAL_AMOUNT, BONDED_DENOM)],
)
.unwrap();
// setup staking parameters
router
.staking
.setup(
storage,
StakingInfo {
bonded_denom: BONDED_DENOM.to_string(),
unbonding_time: UNBONDING_TIME,
apr: Decimal::percent(10),
},
)
.unwrap();
// add a validator
router
.staking
.add_validator(api, storage, &block, valoper)
.unwrap();
});
// Delegator address has chain's prefix.
let delegator_addr = app.api().addr_make(DELEGATOR);
// delegate tokens to validator
app.execute(
delegator_addr.clone(),
StakingMsg::Delegate {
validator: validator_addr.to_string(),
amount: coin(DELEGATION_AMOUNT, BONDED_DENOM),
}
.into(),
)
.unwrap();
// delegation works immediately, so delegator should have now fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());
// validator should have now DELEGATION_AMOUNT of tokens assigned
let delegation = app
.wrap()
.query_delegation(delegator_addr.clone(), validator_addr.clone())
.unwrap()
.unwrap();
assert_eq!(DELEGATION_AMOUNT, delegation.amount.amount.u128());
// now, undelegate all bonded tokens
app.execute(
delegator_addr.clone(),
StakingMsg::Undelegate {
validator: validator_addr.to_string(),
amount: coin(DELEGATION_AMOUNT, BONDED_DENOM),
}
.into(),
)
.unwrap();
// unbonding works with timeout, so tokens will be given back
// after unbonding time; while we do not change the block height
// and time, delegator should still have fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());
// now we update the block height and time that is shorter
// than the unbonding time
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_seconds(UNBONDING_TIME - 1);
});
// delegator should still have fewer tokens
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(FEWER_AMOUNT, delegator_balance.amount.u128());
// now we update the block height and time again,
// so unbonding time is reached
app.update_block(|block| {
block.height += 1;
block.time = block.time.plus_seconds(1);
});
// delegator should now have the initial amount of tokens back
let delegator_balance = app
.wrap()
.query_balance(delegator_addr.clone(), BONDED_DENOM)
.unwrap();
assert_eq!(INITIAL_AMOUNT, delegator_balance.amount.u128());
// there should be no more delegations
let delegation = app
.wrap()
.query_delegation(delegator_addr, validator_addr)
.unwrap();
assert_eq!(None, delegation);