First Integration Test

So far, you have tested your smart contract with unit tests. These unit tests help you verify that your functions work as expected in isolation. However, they do not let you check whether your smart contract would work correctly on a blockchain, or with each other.

You are fixing that in this section.

Exercise progression

If you skipped the previous section, you can just switch the project to its first-query-message branch and take it from there.

Structure

You are going to use MultiTest, which mocks an underlying blockchain, complete with mocked modules such as Bank. These mocks and tools are still all in Rust, which ensures speed. They would also allow you to test cross-contract communication.

Because the tests take place in Rust, there is no compilation to WebAssembly. So to mimic a compiled object, there is a ContractWrapper that exposes functions as if they were your smart contract's entry points.

There is neither networking, consensus nor block creation.

In this section, each of your integration test will:

  • Mock an underlying app chain.
  • Store your smart contract code.
  • Deploy a smart contract instance.
  • Test something specific on this instance.
  • Verify that it happened as per the expectations.

Dependencies

You start by adding MultiTest as a development dependency to your project:

cargo add --dev cw-multi-test@2.1.1

You are going to put your integration tests into a new folder: tests. In this folder, create a contract.rs file where you start by adding your dependencies:

tests/contract.rs
use cosmwasm_std::Addr;
use cw_multi_test::{App, ContractWrapper, Executor};
use cw_my_nameservice::{
    contract::{execute, instantiate, query},
    msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ResolveRecordResponse},
};

Note that:

  • cw_multi_test::App mocks an underlying Cosmos app chain.
  • cw_multi_test::ContractWrapper mocks a compiled smart contract, without actually compiling it to WebAssembly.
  • cw_multi_test::Executor imports functions that allow you to execute actions on your mocked App.
  • You import your smart contract's functions and messages. You may have to rename from my_nameservice if you picked a different name for your project.

Preparation

When you mock your underlying app chain, you can choose which features it should implement. In this case, the defaults will be enough. So to mock an App, you simply call:

let mut mock_app = App::default();

Each of your tests will repeat similar steps, namely:

  1. Wrap the smart contract functions, to simulate a compilation.
  2. Store the code on the mocked app chain.
  3. Deploy an instance of your smart contract.

So it is worth creating a function that you can call to do that. Add to tests/contract.rs:

tests/contract.rs
fn instantiate_nameservice(mock_app: &mut App) -> (u64, Addr) {
    let nameservice_code = Box::new(ContractWrapper::new(execute, instantiate, query));
    let nameservice_code_id = mock_app.store_code(nameservice_code);
    return (
        nameservice_code_id,
        mock_app
            .instantiate_contract(
                nameservice_code_id,
                Addr::unchecked("deployer"),
                &InstantiateMsg {},
                &[],
                "nameservice",
                None,
            )
            .expect("Failed to instantiate nameservice"),
    );
}

Note how:

  • Your smart contract is "compiled" into ContractWrapper.
  • It is then stored on-chain, at a code id.
  • The address of the deployer is not important, but could be in future iterations of your smart contract.
  • The instantiate_contract function is actually defined in Executor.

Name register test

With this, you can add a test of a name register. You want to make sure that it is saved to storage. Add:

tests/contract.rs
#[test]
fn test_register() {
    // Arrange
    let mut mock_app = App::default();
    let (_, contract_addr) = instantiate_nameservice(&mut mock_app);
    let owner_addr_value = "owner".to_owned();
    let owner_addr = Addr::unchecked(owner_addr_value.clone());
    let name_alice = "alice".to_owned();
    let register_msg = ExecuteMsg::Register {
        name: name_alice.to_owned(),
    };

    // Act
    let result = mock_app.execute_contract(
        owner_addr.clone(),
        contract_addr.clone(),
        &register_msg,
        &[],
    );

    // Assert
    assert!(result.is_ok(), "Failed to register alice");
    let stored_addr_bytes = mock_app
        .contract_storage(&contract_addr)
        .get(format!("\0\rname_resolver{name_alice}").as_bytes())
        .expect("Failed to load from name alice");
    let stored_addr = String::from_utf8(stored_addr_bytes).unwrap();
    assert_eq!(stored_addr, format!(r#"{{"owner":"{owner_addr_value}"}}"#));
}

Note that:

  • It looks very much like what you did in unit tests.
  • The execute_contract is declared in Executor.
  • You are accessing directly to storage, which is a bit arduous but could come in handy at times, instead of relying on the query message. The "alice" key is prefixed with the 0 and \r bytes and the name of the storage map.

To confirm that it works, you run the same way you did for unit tests:

cargo test

Which should print something like:

...
     Running tests/contract.rs (target/debug/deps/contract-d6161d38a3b0d331)

running 1 test
test test_register ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
...

Name query test

With the name register test in place, you can add another test to confirm that the query works too. Add:

tests/contract.rs
#[test]
fn test_query() {
    // Arrange
    let mut mock_app = App::default();
    let (_, contract_addr) = instantiate_nameservice(&mut mock_app);
    let owner_addr = Addr::unchecked("owner");
    let name_alice = "alice".to_owned();
    let register_msg = ExecuteMsg::Register {
        name: name_alice.to_owned(),
    };
    let _ = mock_app
        .execute_contract(
            owner_addr.clone(),
            contract_addr.clone(),
            &register_msg,
            &[],
        )
        .expect("Failed to register alice");
    let resolve_record_query_msg = QueryMsg::ResolveRecord {
        name: name_alice.to_owned(),
    };

    // Act
    let result = mock_app
        .wrap()
        .query_wasm_smart::<ResolveRecordResponse>(&contract_addr, &resolve_record_query_msg);

    // Assert
    assert!(result.is_ok(), "Failed to query alice name");
    assert_eq!(
        result.unwrap(),
        ResolveRecordResponse {
            address: Some(owner_addr.to_string())
        }
    )
}

Note that:

  • This time you execute the register command and expect a positive result, instead of checking it with an assert!.
  • You access the query functions by wrapping the app: .wrap().
  • There are a lot of possible query functions. So as to handle the fewer de/serialization matters, you can call query_wasm_smart.
  • query_wasm_smart is a function of the mocked app, so it expects you to pass the address of the contract to query.
  • You also need to specify the expected ResolveRecordResponse type because the compiler cannot otherwise infer it.

Once you run cargo test again, you should see:

...
running 2 tests
test test_register ... ok
test test_query ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
...

For good measure, you can add a test that makes sure there are no results when querying on an unregistered name:

tests/contract.rs
#[test]
fn test_query_empty() {
    // Arrange
    let mut mock_app = App::default();
    let (_, contract_addr) = instantiate_nameservice(&mut mock_app);
    let name_alice = "alice".to_owned();
    let resolve_record_query_msg = QueryMsg::ResolveRecord {
        name: name_alice.to_owned(),
    };

    // Act
    let result = mock_app
        .wrap()
        .query_wasm_smart::<ResolveRecordResponse>(&contract_addr, &resolve_record_query_msg);

    // Assert
    assert!(result.is_ok(), "Failed to query alice name");
    assert_eq!(result.unwrap(), ResolveRecordResponse { address: None })
}

Conclusion

You have created your first mocked-app test whereby your smart contract is tested against a mocked CosmWasm module.

Exercise progression

At this stage, you should have something similar to the first-multi-test branch, with this as the diff.