Writing tests in Sylvia

As a reminder, the file structure of the counter project written in Sylvia is shown below. In this chapter, you will be writing tests for this smart contract. All tests will be placed in a file named test_counter.rs, highlighted in line 12.

counter (directory)
.
├── Cargo.toml
├── coverage.sh
├── src
│   ├── contract.rs
│   ├── lib.rs
│   └── msg.rs
└── tests
    ├── mod.rs
    └── multitest
        ├── mod.rs
        └── test_counter.rs
🏃

If you're in a hurry, you can find the final version of the test_counter.rs file at the end of this chapter.

Imports

At the very beginning of the test_counter.rs file, the necessary imports for implementing all test cases are included:

  • structures generated by Sylvia (CodeId, CounterContractProxy),
  • initialization message for the contract (CounterInitMsg),
  • a trait for converting a string into address (IntoAddr),
  • and the Sylvia app (chain) struct that simulates the real-life blockchain (sylvia::multitest::App).

Just copy and paste the code presented below to your test_counter.rs file:

test_counter.rs
use counter::contract::sv::mt::{CodeId, CounterContractProxy};
use counter::msg::CounterInitMsg;
use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;

Testing counter initialization

There are two ways to initialize the counter contact:

  • with zero value,
  • with specific value in range [0..255].

Testing initialization with zero

This first test verifies that the counter contract can be instantiated with an initial value of zero using the CounterInitMsg::Zero message. It ensures that the contract correctly stores the initial value of zero, and that the query mechanism returns the expected counter value.

It is an example of a well-structured test case with clear setup, execution, and validation steps.

test_counter.rs
#[test]
fn instantiating_with_zero_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Zero)
        .call(&owner)
        .unwrap();
 
    assert_eq!(0, contract.value().unwrap().value);
}

Let's take a closer look at this test's flow.

Chain initialization

Creating an instance of the App simulates starting a blockchain. There are multiple ways to instantiate the App, but the simplest way, shown below, uses the default function.

test_counter.rs
let app = App::default();

Storing contract on chain

The next step is to store the contract's code on the chain. The CodeId struct generated by Sylvia for the counter contract provides a store_code function for this purpose. store_code returns a handle to the contract assigned to code_id. This code_id is used to reference the stored contract later in test.

test_counter.rs
let code_id = CodeId::store_code(&app);

Defining the contract owner

Every instance of the contract has an owner represented as an address. In this test the string "owner" is converted to Bech32 address recognized by the blockchain.

test_counter.rs
let owner = "owner".into_addr();

Contract instantiation

The contract is to be instantiated using instantiate function provided by code_id with the initialization message variant CounterInitMsg::Zero. instantiate function returns an InstantiateProxy generated by Sylvia, which in turn provides a function call that triggers the instantiation of the contract. The call function gets a single argument being the address of the owner of the contract instance.

The unwrap() function is used to handle the result of instantiating the contract, ensuring the test fails if an error occurs. call function returns a proxy to contract instance, used later in this test.

test_counter.rs
let contract = code_id
    .instantiate(CounterInitMsg::Zero)
    .call(&owner)
    .unwrap();

Querying the contract

After the contract is instantiated, this test queries the contract instance using the value() function, which is a method in the CounterContractProxy generated by Sylvia from CounterContract impl block. Using Sylvia framework, querying a contract is as simple as calling a function on the contract instance. The result from the value() function is simply unwrapped and resolves to CounterResponse which in turn holds the current value of the counter in the public field named value.

test_counter.rs
assert_eq!(0, contract.value().unwrap().value);

Asserting the value

The final step is to assert that the value returned by the contract is 0. If the value were non-zero, the test would fail.

test_counter.rs
assert_eq!(0, contract.value().unwrap().value);

Running tests

Having the first test ready, let's run it:

TERMINAL
cargo test

The expected output should look like the example shown below. Note that only the results of integration tests are shown. For brevity, the results of unit tests and documentation tests are omitted, as we are focusing only on integration tests in this example.

OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 1 test
test multitest::test_counter::instantiating_with_zero_should_work ... ok
 
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Now, let's run all tests using cargo-nextest (opens in a new tab):

TERMINAL
cargo nextest run

Similarly, one test passes:

OUTPUT
    Finished `test` profile [unoptimized + debuginfo] target(s) in 27.67s
    Starting 1 test across 2 binaries (run ID: b2a72818-8d25-4194-9333-3af93c360132, nextest profile: default)
        PASS [   0.004s] counter::mod multitest::test_counter::instantiating_with_zero_should_work
------------
     Summary [   0.004s] 1 test run: 1 passed, 0 skipped

For brevity, in the upcoming test cases in this chapter, we will skip running tests using cargo-nextest (opens in a new tab). However, you can always run all tests by typing cargo nextest run.

Code coverage

Let's check the code coverage after adding the first test:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 8/22
||
36.36% coverage, 8/22 lines covered

First test covered almost 40% of the code in the counter smart contract. The detailed code coverage report (similar to the one generated by Tarpaulin) is shown below.

CODE COVERAGE REPORT (Sylvia version)
 use crate::msg::{CounterInitMsg, CounterResponse};
 use cosmwasm_std::{Response, StdResult};
 use cw_storage_plus::Item;
 use sylvia::contract;
 use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
 
 pub struct CounterContract {
     pub count: Item<u8>,
 }
 
 #[cfg_attr(not(feature = "library"), sylvia::entry_points)]
 #[contract]
 impl CounterContract {
     pub const fn new() -> Self {
         Self {
             count: Item::new("count"),
         }
     }
 
     #[sv::msg(instantiate)]
     fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
         match msg {
             CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
             CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
         }
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_add(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_sub(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
         self.count.save(ctx.deps.storage, &value)?;
         Ok(Response::new())
     }
 
     #[sv::msg(query)]
     fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
         let value = self.count.load(ctx.deps.storage)?;
         Ok(CounterResponse { value })
     }
 }

The code coverage report reflects exactly the scope of the first test. The init() and value() functions of the contract were executed. There is still one more message variant (line 24) to be tested during contract instantiation and this will be covered in the next test. The functions inc(), dec() and set() were not called in this test and still shine red in coverage report.

Testing initialization with a specific value

The second test verifies the initialization of the counter contract using a specific value that must be in range [0..255]. For the purpose of this example, let say it will be 12. Initialization with a specific value is done using the CounterInitMsg::Set message.

test_counter.rs
#[test]
fn instantiating_with_value_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(12))
        .call(&owner)
        .unwrap();
 
    assert_eq!(12, contract.value().unwrap().value);
}

Except for the message used to instantiate the contract and the value assertion, all steps in this test are the same as in the previous one, let's summarize them shortly:

Contract instantiation

This time the contract is instantiated using CounterInitMsg::Set message passed to instantiate function provided by code_id:

test_counter.rs
let contract = code_id
    .instantiate(CounterInitMsg::Set(12))
    .call(&owner)
    .unwrap();

Asserting the value

The expected value of the counter is 12 in this test.

test_counter.rs
assert_eq!(12, contract.value().unwrap().value);

Running tests

To execute all test type in terminal:

TERMINAL
cargo test

The expected output should be similar to the one shown below. Both tests pass.

OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 2 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
 
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

To make sure that the entire init() function of the counter contract is tested, let's run the code coverage:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 9/22
||
40.91% coverage, 9/22 lines covered

The detailed code coverage report (similar to the one generated by Tarpaulin) is shown below:

CODE COVERAGE REPORT (Sylvia version)
 use crate::msg::{CounterInitMsg, CounterResponse};
 use cosmwasm_std::{Response, StdResult};
 use cw_storage_plus::Item;
 use sylvia::contract;
 use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
 
 pub struct CounterContract {
     pub count: Item<u8>,
 }
 
 #[cfg_attr(not(feature = "library"), sylvia::entry_points)]
 #[contract]
 impl CounterContract {
     pub const fn new() -> Self {
         Self {
             count: Item::new("count"),
         }
     }
 
     #[sv::msg(instantiate)]
     fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
         match msg {
             CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
             CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
         }
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_add(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_sub(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
         self.count.save(ctx.deps.storage, &value)?;
         Ok(Response::new())
     }
 
     #[sv::msg(query)]
     fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
         let value = self.count.load(ctx.deps.storage)?;
         Ok(CounterResponse { value })
     }
 }

As expected, the init() function is fully tested, including all initialization message variants. However, the inc(), dec() and set() functions still shine red. Let's address this in the next test case.

Testing counter increment

Testing increment by 1

In this test, the contract is instantiated with an initial value of zero, similar to the first test. However, this time, an increment action is performed by invoking the inc() method from generated CounterContractProxy trait. This action is expected to increment the counter by 1. Finally, the counter's value is queried to confirm that it has been correctly incremented and now holds the value 1.

test_counter.rs
#[test]
fn incrementing_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Zero)
        .call(&owner)
        .unwrap();
 
    contract.inc().call(&owner).unwrap();
 
    assert_eq!(1, contract.value().unwrap().value);
}

You should already be familiar with the flow of this test; the only difference from the previous examples is the additional step of invoking the inc() function.

Executing the increment action

test_counter.rs
contract.inc().call(&owner).unwrap();

The inc() function of the CounterContractProxy trait is evaluated by executing the call function with the address of the contract owner. The result is simply unwrapped in this test.

Running tests

Let's execute all tests by typing in the terminal:

TERMINAL
cargo test

All 3 tests should pass:

OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 3 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
test multitest::test_counter::incrementing_should_work ... ok
 
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

Like in the previous examples let's run the code coverage script:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 14/22
||
63.64% coverage, 14/22 lines covered

The code coverage report (similar to the one generated by Tarpaulin) is as follows:

CODE COVERAGE REPORT (Sylvia version)
 use crate::msg::{CounterInitMsg, CounterResponse};
 use cosmwasm_std::{Response, StdResult};
 use cw_storage_plus::Item;
 use sylvia::contract;
 use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
 
 pub struct CounterContract {
     pub count: Item<u8>,
 }
 
 #[cfg_attr(not(feature = "library"), sylvia::entry_points)]
 #[contract]
 impl CounterContract {
     pub const fn new() -> Self {
         Self {
             count: Item::new("count"),
         }
     }
 
     #[sv::msg(instantiate)]
     fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
         match msg {
             CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
             CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
         }
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_add(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_sub(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
         self.count.save(ctx.deps.storage, &value)?;
         Ok(Response::new())
     }
 
     #[sv::msg(query)]
     fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
         let value = self.count.load(ctx.deps.storage)?;
         Ok(CounterResponse { value })
     }
 }

As expected, the inc() function was called and the counter value was incremented by 1. Notice that there are two additional functions dec() and set, that still need to be tested for this contract. However, before we proceed, let's address the issue of counter overflow during incrementation.

Testing increment overflow

When you recall the counter smart contract specification, you will notice that the counter value is of type u8, which means that the maximum value this counter can hold is equal to 255. What happens when you increment the counter beyond 255? The following test initializes the counter with value 250 and then increments it 10 times by calling the inc() function.

test_counter.rs
#[test]
fn incrementing_should_stop_at_maximum() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(250))
        .call(&owner)
        .unwrap();
 
    for _ in 1..=10 {
        contract.inc().call(&owner).unwrap();
    }
 
    assert_eq!(255, contract.value().unwrap().value);
}

In the highlighted lines 69 to 71, the loop is executed 10 times, after which the counter value is queried and asserted to be 255. As you will see, this test will pass, indicating that the counter stops incrementing once it reaches value 255. This happens because the counter is incremented using the saturating_add function on the u8 type.

As an exercise, you can modify this test by initializing the counter with zero and incrementing it, let say, 1000 times. This will demonstrate how quick and simple it is to test boundary values on constrained types using MultiTest.

Running tests

Make sure all tests pass:

TERMINAL
cargo test
OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 4 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
test multitest::test_counter::incrementing_should_work ... ok
test multitest::test_counter::incrementing_should_stop_at_maximum ... ok
 
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

We check now the code coverage, as usual:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 14/22
||
63.64% coverage, 14/22 lines covered

The code coverage did not change after adding this test case, but a very important use case was tested: handling overflow during counter incrementation.

Testing counter decrement

Testing decrement by 1

The next function to be tested is named dec(). Calling it should decrement the counter by 1. The following test is similar to the one you wrote for testing counter incrementation. The changed lines are highlighted in the following code snippet:

test_counter.rs
#[test]
fn decrementing_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(126))
        .call(&owner)
        .unwrap();
 
    contract.dec().call(&owner).unwrap();
 
    assert_eq!(125, contract.value().unwrap().value);
}

In line 85 the counter is initialized with the value 126, then in line 89 it is decremented by 1 and in line 91 it is asserted that the final value of the counter is 125.

Running tests

The only way to confirm that the counter is properly decremented is by running all the tests:

TERMINAL
cargo test
OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 5 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
test multitest::test_counter::incrementing_should_work ... ok
test multitest::test_counter::incrementing_should_stop_at_maximum ... ok
test multitest::test_counter::decrementing_should_work ... ok
 
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

Run code coverage script, as usual:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 19/22
||
86.36% coverage, 19/22 lines covered

Check the code coverage report:

CODE COVERAGE REPORT (Sylvia version)
 use crate::msg::{CounterInitMsg, CounterResponse};
 use cosmwasm_std::{Response, StdResult};
 use cw_storage_plus::Item;
 use sylvia::contract;
 use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
 
 pub struct CounterContract {
     pub count: Item<u8>,
 }
 
 #[cfg_attr(not(feature = "library"), sylvia::entry_points)]
 #[contract]
 impl CounterContract {
     pub const fn new() -> Self {
         Self {
             count: Item::new("count"),
         }
     }
 
     #[sv::msg(instantiate)]
     fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
         match msg {
             CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
             CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
         }
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_add(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_sub(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
         self.count.save(ctx.deps.storage, &value)?;
         Ok(Response::new())
     }
 
     #[sv::msg(query)]
     fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
         let value = self.count.load(ctx.deps.storage)?;
         Ok(CounterResponse { value })
     }
 }

As expected, the dec() function is already tested.

Testing decrement underflow

Similar to the incrementation overflow test, the following test checks for underflow during counter decrementation. The counter is initialized to 5, decremented 10 times, and the final value is asserted to be 0. This test passes without errors because the u8 type in the smart contract is decremented using the saturating_sub function.

test_counter.rs
#[test]
fn decrementing_should_stop_at_minimum() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(5))
        .call(&owner)
        .unwrap();
 
    for _ in 1..=10 {
        contract.dec().call(&owner).unwrap();
    }
 
    assert_eq!(0, contract.value().unwrap().value);
}

Running tests

Like in previous examples, let's run all tests:

TERMINAL
cargo test
OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 6 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
test multitest::test_counter::incrementing_should_work ... ok
test multitest::test_counter::incrementing_should_stop_at_maximum ... ok
test multitest::test_counter::decrementing_should_work ... ok
test multitest::test_counter::decrementing_should_stop_at_minimum ... ok
 
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

Run the code coverage script, again:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 19/22
||
86.36% coverage, 19/22 lines covered

The code coverage did not change after adding this test case, but a very important use case was tested: handling underflow during counter decrementation.

Testing counter value changes

The remaining red lines in the test coverage report highlight the set() function. The test below sets the counter directly to 125 (line 127), and the final value of the counter is expected to be 125.

test_counter.rs
#[test]
fn setting_value_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(5))
        .call(&owner)
        .unwrap();
 
    contract.set(125).call(&owner).unwrap();
 
    assert_eq!(125, contract.value().unwrap().value);
}

Running tests

Let’s run tests:

TERMINAL
cargo test

All tests should pass:

OUTPUT
     Running tests/mod.rs (target/debug/deps/mod-a819a838d5b2d67d)
 
running 7 tests
test multitest::test_counter::instantiating_with_zero_should_work ... ok
test multitest::test_counter::instantiating_with_value_should_work ... ok
test multitest::test_counter::incrementing_should_work ... ok
test multitest::test_counter::incrementing_should_stop_at_maximum ... ok
test multitest::test_counter::decrementing_should_work ... ok
test multitest::test_counter::decrementing_should_stop_at_minimum ... ok
test multitest::test_counter::setting_value_should_work ... ok
 
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Code coverage

Let's generate the final code coverage report:

TERMINAL
./coverage.sh
OUTPUT
|| Tested/Total Lines:
|| src/contract.rs: 22/22
||
100.00% coverage, 22/22 lines covered

Nice, you have reached 💯% code coverage.

CODE COVERAGE REPORT (Sylvia version)
 use crate::msg::{CounterInitMsg, CounterResponse};
 use cosmwasm_std::{Response, StdResult};
 use cw_storage_plus::Item;
 use sylvia::contract;
 use sylvia::ctx::{ExecCtx, InstantiateCtx, QueryCtx};
 
 pub struct CounterContract {
     pub count: Item<u8>,
 }
 
 #[cfg_attr(not(feature = "library"), sylvia::entry_points)]
 #[contract]
 impl CounterContract {
     pub const fn new() -> Self {
         Self {
             count: Item::new("count"),
         }
     }
 
     #[sv::msg(instantiate)]
     fn init(&self, ctx: InstantiateCtx, msg: CounterInitMsg) -> StdResult<Response> {
         match msg {
             CounterInitMsg::Zero => self.count.save(ctx.deps.storage, &0)?,
             CounterInitMsg::Set(value) => self.count.save(ctx.deps.storage, &value)?,
         }
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn inc(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_add(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn dec(&self, ctx: ExecCtx) -> StdResult<Response> {
         self.count
             .update(ctx.deps.storage, |count| -> StdResult<u8> {
                 Ok(count.saturating_sub(1))
             })?;
         Ok(Response::new())
     }
 
     #[sv::msg(exec)]
     fn set(&self, ctx: ExecCtx, value: u8) -> StdResult<Response> {
         self.count.save(ctx.deps.storage, &value)?;
         Ok(Response::new())
     }
 
     #[sv::msg(query)]
     fn value(&self, ctx: QueryCtx) -> StdResult<CounterResponse> {
         let value = self.count.load(ctx.deps.storage)?;
         Ok(CounterResponse { value })
     }
 }

All functionalities of the counter smart contract has been tested, and the code coverage report shines green.

Test cases put all together

Below is the final version of the test_counter.rs file, containing all previously presented test cases for the counter smart contract written using the Sylvia framework.

test_counter.rs
use counter::contract::sv::mt::{CodeId, CounterContractProxy};
use counter::msg::CounterInitMsg;
use sylvia::cw_multi_test::IntoAddr;
use sylvia::multitest::App;
 
#[test]
fn instantiating_with_zero_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Zero)
        .call(&owner)
        .unwrap();
 
    assert_eq!(0, contract.value().unwrap().value);
}
 
#[test]
fn instantiating_with_value_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(12))
        .call(&owner)
        .unwrap();
 
    assert_eq!(12, contract.value().unwrap().value);
}
 
#[test]
fn incrementing_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Zero)
        .call(&owner)
        .unwrap();
 
    contract.inc().call(&owner).unwrap();
 
    assert_eq!(1, contract.value().unwrap().value);
}
 
#[test]
fn incrementing_should_stop_at_maximum() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(250))
        .call(&owner)
        .unwrap();
 
    for _ in 1..=10 {
        contract.inc().call(&owner).unwrap();
    }
 
    assert_eq!(255, contract.value().unwrap().value);
}
 
#[test]
fn decrementing_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(126))
        .call(&owner)
        .unwrap();
 
    contract.dec().call(&owner).unwrap();
 
    assert_eq!(125, contract.value().unwrap().value);
}
 
#[test]
fn decrementing_should_stop_at_minimum() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(5))
        .call(&owner)
        .unwrap();
 
    for _ in 1..=10 {
        contract.dec().call(&owner).unwrap();
    }
 
    assert_eq!(0, contract.value().unwrap().value);
}
 
#[test]
fn setting_value_should_work() {
    let app = App::default();
 
    let code_id = CodeId::store_code(&app);
 
    let owner = "owner".into_addr();
 
    let contract = code_id
        .instantiate(CounterInitMsg::Set(5))
        .call(&owner)
        .unwrap();
 
    contract.set(125).call(&owner).unwrap();
 
    assert_eq!(125, contract.value().unwrap().value);
}

Summary

In this chapter, you learned how to test a smart contract written in Sylvia, how to properly structure effective test cases, and how to measure testing progress with code coverage reports. You also became familiar with the cargo test and cargo nextest run tools. We recommend trying to write tests for a smart contract built using pure CosmWasm libraries to experience the differences between these two approaches.