Testing
Testing are vital to smart contract development, every developer should be implementing tests and striving to maximize test coverage. Tests are what give you and your protocol sleep at night and ensure changes can be rapidly deployed to the contracts codebase without breaking everything else. A great set of contracts will have a great set of tests generally divided into two areas of testing. Unit testing and Integration testing.
But in this aura-nft project, because of its logic simplicity, we will only need write unit tests. About Integration Testing
, you can read more here.
So let's look at our contract.rs
file, at the bottom, there should be something that looks like this:
#[cfg(test)]
mod tests {}
This module is where we will implement our tests, the flag above shows it is for testing.
Here we have a total of 10 unit tests. Their code is quite long so I will only choose 1 important unit test to explain to you about the idea and syntax. The other unit tests, I will describe what they test, all you need to do is refer to the source code and try to rewrite them yourself.
Our test module will start with the imports and global helper variables.
use super::*; // using the entire function of this contract
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}; // mock functions to mock an environment, message info, dependencies, contract address
use cosmwasm_std::{from_binary, to_binary, CosmosMsg, SubMsgResponse, SubMsgResult}; // functions and structs from `cosmwasm::std` we will use in the unit tests
use prost::Message; // A Protocol Buffers message. `prost` is a Protocol Buffers implementation for the Rust Language
const NFT_CONTRACT_ADDR: &str = "nftcontract"; // Fake address we will use to mock_info of cw721_address
// Type for replies to contract instantiate messes
#[derive(Clone, PartialEq, Message)]
struct MsgInstantiateContractResponse {
#[prost(string, tag = "1")]
pub contract_address: ::prost::alloc::string::String,
#[prost(bytes, tag = "2")]
pub data: ::prost::alloc::vec::Vec<u8>,
}
Mocking is faking values for the sake of testing. For example, mock_dependencies
can fake the value needed for the DepsMut
type for our instantiate call.
mock_env
can return a default environment with height, time, chain_id, and contract address You can submit as is to most contracts, or modify height/time if you want to test for expiration. mock_info
can set sender and funds for the message. All of these are intended for use in test code only.
Initialization unit test
And this is initialization
unit test. We will explain it in detail.
#[test]
fn initialization() {
let mut deps = mock_dependencies();
let msg = InstantiateMsg {
owner: Addr::unchecked("owner"),
max_tokens: 1,
unit_price: Uint128::new(1),
name: String::from("SYNTH"),
symbol: String::from("SYNTH"),
token_code_id: 10u64,
cw20_address: Addr::unchecked(MOCK_CONTRACT_ADDR),
token_uri: String::from("https://ipfs.io/ipfs/Q"),
extension: None,
};
let info = mock_info("owner", &[]);
let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();
instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
assert_eq!(
res.messages,
vec![SubMsg {
msg: WasmMsg::Instantiate {
code_id: msg.token_code_id,
msg: to_binary(&Cw721InstantiateMsg {
name: msg.name.clone(),
symbol: msg.symbol.clone(),
minter: MOCK_CONTRACT_ADDR.to_string(),
})
.unwrap(),
funds: vec![],
admin: None,
label: String::from("Instantiate fixed price NFT contract"),
}
.into(),
id: INSTANTIATE_TOKEN_REPLY_ID,
gas_limit: None,
reply_on: ReplyOn::Success,
}]
);
let instantiate_reply = MsgInstantiateContractResponse {
contract_address: "nftcontract".to_string(),
data: vec![2u8; 32769],
};
let mut encoded_instantiate_reply =
Vec::<u8>::with_capacity(instantiate_reply.encoded_len());
instantiate_reply
.encode(&mut encoded_instantiate_reply)
.unwrap();
let reply_msg = Reply {
id: INSTANTIATE_TOKEN_REPLY_ID,
result: SubMsgResult::Ok(SubMsgResponse {
events: vec![],
data: Some(encoded_instantiate_reply.into()),
}),
};
reply(deps.as_mut(), mock_env(), reply_msg).unwrap();
let query_msg = QueryMsg::GetConfig {};
let res = query(deps.as_ref(), mock_env(), query_msg).unwrap();
let config: Config = from_binary(&res).unwrap();
assert_eq!(
config,
Config {
owner: Addr::unchecked("owner"),
cw20_address: msg.cw20_address,
cw721_address: Some(Addr::unchecked(NFT_CONTRACT_ADDR)),
max_tokens: msg.max_tokens,
unit_price: msg.unit_price,
name: msg.name,
symbol: msg.symbol,
token_uri: msg.token_uri,
extension: None,
unused_token_id: 0
}
);
}
The instantiate
unit test is divided into 2 parts, the first part checks if response of instantiate
function is in the correct format, the second part checks if the reply
handling is logical or not.
Part 1
The first statement is used to mock the dependencies, must be mutable so we can pass it as a mutable, empty vector means our contract has no balance.
let mut deps = mock_dependencies();
The second statement is used to create a InstantiateMsg
message.
let msg = InstantiateMsg {
owner: Addr::unchecked("owner"),
max_tokens: 1,
unit_price: Uint128::new(1),
name: String::from("SYNTH"),
symbol: String::from("SYNTH"),
token_code_id: 10u64,
cw20_address: Addr::unchecked(MOCK_CONTRACT_ADDR),
token_uri: String::from("https://ipfs.io/ipfs/Q"),
extension: None,
};
The third statement is used to mock the message info, "owner" will be the sender, the empty vec means we sent no funds.
let info = mock_info("owner", &[]);
The next, is used to call instantiate, unwrap to assert success.
let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();
instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();
Because the instantiate
function has the return line is Ok(Response::new().add_submessages(sub_msg))
so in here, the assert_eq
macro is used to check if res.messages
(Response of instantiate) equals the sub_msg
or not.
assert_eq!(
res.messages,
vec![SubMsg {
msg: WasmMsg::Instantiate {
code_id: msg.token_code_id,
msg: to_binary(&Cw721InstantiateMsg {
name: msg.name.clone(),
symbol: msg.symbol.clone(),
minter: MOCK_CONTRACT_ADDR.to_string(),
})
.unwrap(),
funds: vec![],
admin: None,
label: String::from("Instantiate fixed price NFT contract"),
}
.into(),
id: INSTANTIATE_TOKEN_REPLY_ID,
gas_limit: None,
reply_on: ReplyOn::Success,
}]
);
Part 2
Please try to explain this part. Brainstorming will help us to understand more deeply. A suggestion is to follow part 1, separate each statement to explain what it does, and also, go back to the reply
function code to explain why need to encode the instantiate_reply
response to a buffer.
let instantiate_reply = MsgInstantiateContractResponse {
contract_address: "nftcontract".to_string(),
data: vec![2u8; 32769],
};
let mut encoded_instantiate_reply =
Vec::<u8>::with_capacity(instantiate_reply.encoded_len());
instantiate_reply
.encode(&mut encoded_instantiate_reply)
.unwrap();
let reply_msg = Reply {
id: INSTANTIATE_TOKEN_REPLY_ID,
result: SubMsgResult::Ok(SubMsgResponse {
events: vec![],
data: Some(encoded_instantiate_reply.into()),
}),
};
reply(deps.as_mut(), mock_env(), reply_msg).unwrap();
let query_msg = QueryMsg::GetConfig {};
let res = query(deps.as_ref(), mock_env(), query_msg).unwrap();
let config: Config = from_binary(&res).unwrap();
assert_eq!(
config,
Config {
owner: Addr::unchecked("owner"),
cw20_address: msg.cw20_address,
cw721_address: Some(Addr::unchecked(NFT_CONTRACT_ADDR)),
max_tokens: msg.max_tokens,
unit_price: msg.unit_price,
name: msg.name,
symbol: msg.symbol,
token_uri: msg.token_uri,
extension: None,
unused_token_id: 0
}
);
The remaining unit tests
Here is the explanation of the remaining 9 unit tests:
#[test]
fn invalid_unit_price() {}: Check if an error is returned when unit_price
does not satisfy the condition config.unit_price > 0
#[test]
fn invalid_max_tokens() {}: Check if an error is returned when max_tokens
does not satisfy the condition config.max_tokens > 0
#[test]
fn mint() {}: Check if mint
action is working as experted or not.
#[test]
fn invalid_reply_id() {}: Check if an error is returned when id
of ReplyMsg
does not satisfy the condition id == INSTANTIATE_TOKEN_REPLY_ID
#[test]
fn cw721_already_linked() {}: Check if an error is returned when config.cw721_address
does not satisfy the condition config.cw721_address == None
in reply
function.
#[test]
fn sold_out() {}: Check if an error is returned when unused_token_id
does not satisfy the condition unused_token_id < config.max_tokens
#[test] fn uninitialized() {}: Test token transfer when nft contract (cw721_address) has not been linked (uninitialized)
#[test]
fn unauthorized_token() {}: Check if an error is returned when info.sender
does not satisfy the condition config.cw20_address == info.sender
in execute_receive
function.
#[test]
fn wrong_amount() {}: Check if an error is returned when amount
of Cw20ReceiveMsg
does not satisfy the condition amount == config.unit_price
Run unit tests
Open your terminal:
cargo test
The output will be look like this:
Compiling aura-nft v0.1.0 (/home/ducchinh1912/Work/Aura/aura-nft)
Finished test [unoptimized + debuginfo] target(s) in 1.84s
Running unittests src/lib.rs (target/debug/deps/aura_nft-e49c72ed70329295)
running 10 tests
test contract::tests::invalid_max_tokens ... ok
test contract::tests::invalid_unit_price ... ok
test contract::tests::invalid_reply_id ... ok
test contract::tests::uninitialized ... ok
test contract::tests::sold_out ... ok
test contract::tests::cw721_already_linked ... ok
test contract::tests::initialization ... ok
test contract::tests::wrong_amount ... ok
test contract::tests::unauthorized_token ... ok
test contract::tests::mint ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/bin/schema.rs (target/debug/deps/schema-d0e1a12d00113e5b)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests aura-nft
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Alright, you have successfully run the unit tests. In the next chapter, we will deploy our contract and play with it!