Testing a query
Last time we created a new query. Now it is time to test it out.
We will start with the basics - the unit test. This approach is simple and doesn't require knowledge
besides Rust. Go to the src/contract.rs
and add a test in its module:
// ...
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greet_query() {
let resp = query::greet().unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
If you ever wrote a unit test in Rust, nothing should surprise you here. Just a simple test-only module which contains local function unit tests. The problem is - this test doesn't build yet. We need to tweak our message types a bit.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GreetResp {
pub message: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum QueryMsg {
Greet {},
}
I added three new derives to both message types.
PartialEq
(opens in a new tab) is required to allow comparing
types for equality - so we can check if they are equal.
Debug
(opens in a new tab) is a trait generating debug-printing
utilities. It is used by assert_eq!
(opens in a new tab) to
display information about mismatch if an assertion fails. Note that because we are not testing the
QueryMsg
in any way, the additional trait derives are optional. Still, it is a good practice to
make all messages both PartialEq
and Debug
for testability and consistency. The last one,
Clone
(opens in a new tab) is not needed yet, but it is also
good practice to allow messages to be cloned around. We will also require that later.
Now we are ready to run our test:
cargo test
...
running 1 test
test contract::tests::greet_query ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Yay! Test passed!
Contract as a black box
Now let's go a step further. The Rust testing utility is a friendly tool for building even
higher-level tests. We are currently testing smart contract internals, but think about what your
smart contract looks like from the outside world. It is a single entity that is triggered by some
input messages. We can create tests that treat the whole contract as a black box by testing it via
our query
function. Let's update our test:
// ...
#[cfg(test)]
mod tests {
use cosmwasm_std::from_json;
use cosmwasm_std::testing::{mock_dependencies, mock_env};
use super::*;
#[test]
fn greet_query() {
let resp = query(
mock_dependencies().as_ref(),
mock_env(),
QueryMsg::Greet {}
).unwrap();
let resp: GreetResp = from_json(&resp).unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
We needed to produce two entities for the query
functions: the deps
and env
instances.
Fortunately, cosmwasm-std
provides utilities for testing those -
mock_dependencies
(opens in a new tab)
and mock_env
(opens in a new tab)
functions.
You may notice the dependencies mock is of type
OwnedDeps
(opens in a new tab) instead of
Deps
, which we need here - this is why the
as_ref
(opens in a new tab)
function is called on it. If we needed a DepsMut
object, we would use
as_mut
(opens in a new tab)
instead.
We can rerun the test, and it should still pass. But when we think about that test reflecting the actual use case, it is inaccurate. The contract is queried, but it was never instantiated! In software engineering, it is equivalent to calling a getter without constructing an object - taking it out of nowhere. It is a lousy testing approach. We can do better:
// ...
#[cfg(test)]
mod tests {
use cosmwasm_std::from_json;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use super::*;
#[test]
fn greet_query() {
let mut deps = mock_dependencies();
let env = mock_env();
let sender = "sender".into_addr();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("sender", &[]),
Empty {},
)
.unwrap();
let resp = query(deps.as_ref(), env, QueryMsg::Greet {}).unwrap();
let resp: GreetResp = from_json(&resp).unwrap();
assert_eq!(
resp,
GreetResp {
message: "Hello World".to_owned()
}
);
}
}
A couple of new things here. First, I extracted the deps
and env
variables to their variables
and passed them to calls. The idea is that those variables represent some blockchain persistent
state, and we don't want to create them for every call. We want any changes to the contract state
occurring in instantiate
to be visible in the query
. Also, we want to control how the
environment differs on the query and instantiation.
The info
argument is another story. The message info is unique for each message sent. To create
the info
mock, we must pass two arguments to the
mock_info
(opens in a new tab) function.
First is the address performing the call. It may look strange to pass sender
as an address instead
of some mysterious wasm
followed by hash. For testing purposes, such addresses are typically
better, as they are way more readable in case of failing tests.
The second argument is funds sent with the message. For now, we leave it as an empty slice, as I don't want to talk about token transfers yet - we will cover it later.
So now it is more a real-case scenario. I see just one problem. I say that the contract is a single
black box. But here, nothing connects the instantiate
call to the corresponding query
. It seems
that we assume there is some global contract. But it seems that if we would like to have two
contracts instantiated differently in a single test case, it would become a mess. If only there
would be some tool to abstract this for us, wouldn't it be nice?