Skip to main content

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:

src/contract.rs
#[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!