Skip to main content

Mocking

Mocks are used in tests to exclude functionality that a test doesn't want to test. Mocks are used when it's difficult to test against an external component.

tip

The Soroban Rust SDK makes it just as easy to test against a real contract as it does to test against a mock of a contract. In some ecosystems integration tests are avoided. Not in the Stellar ecosystem. See Integration Tests.

How to Write Tests with Mocks​

The following is an example of a test that uses a mock, written to test the increment-with-pause contract. The contract has an increment function that increases a counter value by one on every invocation. The contract depends on another contract that controls whether the increment functionality is paused.

The following tests set up the increment-with-pause contract, as well as a mock pause contract, and invokes the increment contract's function several times under different conditions the pause contract is expected to be in.

The following test checks that when the pause contract is not paused, the increment contract functions.

#![cfg(test)]
use crate::{Error, IncrementContract, IncrementContractArgs, IncrementContractClient, Pause};
use soroban_sdk::{contract, contractimpl, Env};

mod notpaused {
use super::*;
#[contract]
pub struct Mock;
#[contractimpl]
impl Pause for Mock {
fn paused(_env: Env) -> bool {
false
}
}
}

#[test]
fn test_notpaused() {
let env = Env::default();
let pause_id = env.register(notpaused::Mock, ());
let contract_id = env.register(
IncrementContract,
IncrementContractArgs::__constructor(&pause_id),
);
let client = IncrementContractClient::new(&env, &contract_id);

assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);
}

The following test checks that when the pause contract is paused, the increment contract function rejects attempts to increment.

#![cfg(test)]
use crate::{Error, IncrementContract, IncrementContractArgs, IncrementContractClient, Pause};
use soroban_sdk::{contract, contractimpl, Env};

mod paused {
use super::*;
#[contract]
pub struct Mock;
#[contractimpl]
impl Pause for Mock {
fn paused(_env: Env) -> bool {
true
}
}
}

#[test]
fn test_paused() {
let env = Env::default();
let pause_id = env.register(paused::Mock, ());
let contract_id = env.register(
IncrementContract,
IncrementContractArgs::__constructor(&pause_id),
);
let client = IncrementContractClient::new(&env, &contract_id);

assert_eq!(client.try_increment(), Err(Ok(Error::Paused)));
}

Most tests, whether you're writing unit, mocks, or integration tests, will look very similar to these tests. They'll do four things:

  1. Create an environment, the Env.
  2. Register the contract(s) to be tested.
  3. Invoke functions using the generated client.
  4. Assert the outcome.
warning

Mocking introduces assumptions about the behavior of another contract. Even if the contract publishes an interface that says it'll return a bool (true/false), contracts can return any type.

impl Pause {
fn paused(env: Env) -> ? { ? }
}

This is one reason why it's helpful to test and fuzz using real dependencies and to code defensively assuming any external contract call could cause your contract to fail.

See Integration Tests for how to test with real dependencies.

The Soroban Rust SDK handles contract calls defensively so that any unexpected error or unexpected type returned from the called contract will cause execution to stop. The SDK also provides methods to make these calls and to intercept error situations. See Making Cross-Contract Calls for more details.