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:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[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 contains local function unit tests. The problem is - this test doesn't build yet. We need to tweak our message types a bit. Update the src/msg.rs:

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 is required to allow comparing types for equality - so we can check if they are equal. The Debug is a trait generating debug-printing utilities. It is used by assert_eq! 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 is not needed for now yet, but it is also good practice to allow messages to be cloned around. We will also require that later, so I added it already not to go back and forth.

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 if you think about how your smart contract is visible 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:

use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::from_binary;
    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_binary(&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 and mock_env functions.

You may notice the dependencies mock of a type OwnedDeps instead of Deps, which we need here - this is why the as_ref function is called on it. If we looked for a DepsMut object, we would use as_mut 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:


use crate::msg::{GreetResp, QueryMsg};
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult,
};

pub fn instantiate(
    _deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    _msg: Empty,
) -> StdResult<Response> {
    Ok(Response::new())
}

pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    use QueryMsg::*;

    match msg {
        Greet {} => to_binary(&query::greet()?),
    }
}

mod query {
    use super::*;

    pub fn greet() -> StdResult<GreetResp> {
        let resp = GreetResp {
            message: "Hello World".to_owned(),
        };

        Ok(resp)
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::from_binary;
    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();

        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_binary(&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 function.

First is the address performing a call. It may look strange to pass sender as an address instead of some mysterious wasm followed by hash, but it is a valid address. For testing purposes, such addresses are typically better, as they are way more verbose 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?