Unit Testing
Learn how to write and run unit tests for NEAR smart contracts to test individual methods and functions in isolation.
Smart contracts need both unit and integration tests:
- Unit tests exercise methods individually. They are written in Rust alongside the contract and run locally.
- Integration tests deploy the compiled contract to a local Sandbox or testnet and exercise it through transactions.
Use unit tests for contract logic and state changes. Use integration tests for behavior that depends on the runtime, including cross-contract calls, gas, attached deposits, and multiple accounts.
We recommend using both types of tests and testing on testnet before deploying a contract to mainnet.
Unit tests allow you to test the contract methods individually. They are suitable to check the storage is updated correctly, and that methods return their expected values. They are written in Rust alongside the contract's code and execute locally.
To run the unit tests, simply navigate to the contract's folder and run cargo test:
cargo test
[!TIP]
cargo testruns both unit and integration tests present in the project.
Snippet I: Testing a Counter
The tests in the Counter Example rely on basic functions to check that the increment, decrement, and reset methods work properly.
#[test]
fn increment() {
// instantiate a contract variable with the counter at zero
let mut contract = Counter { val: 0 };
contract.increment(None);
assert_eq!(1, contract.get_num());
}
#[test]
fn increment_with_points() {
// instantiate a contract variable with the counter at zero
let mut contract = Counter { val: 0 };
contract.increment(Some(10));
assert_eq!(10, contract.get_num());
}
#[test]
fn decrement() {
let mut contract = Counter { val: 0 };
contract.decrement(None);
assert_eq!(-1, contract.get_num());
}
#[test]
fn decrement_with_points() {
// instantiate a contract variable with the counter at zero
let mut contract = Counter { val: 0 };
contract.decrement(Some(10));
assert_eq!(-10, contract.get_num());
}
#[test]
fn increment_and_reset() {
let mut contract = Counter { val: 0 };
contract.increment(None);
contract.reset();
assert_eq!(0, contract.get_num());
}
[dev-dependencies]
near-sdk = { version = "5.29.0", features = ["unit-testing"] }
Snippet II: Modifying the Context
While doing unit testing you can modify the Environment variables through the VMContextBuilder. This will enable you to, for example, simulate calls from different users, with specific attached deposit and GAS. Here we present a snippet on how we test the donate method from our Donation Example by manipulating the predecessor and attached_deposit.
#[test]
fn donate() {
let mut contract = Contract::init(BENEFICIARY.parse().unwrap());
// Make a donation
set_context("donor_a", ONE_NEAR);
contract.donate();
let first_donation = contract.get_donation_for_account("donor_a".parse().unwrap());
// Check the donation was recorded correctly
assert_eq!(
u128::from(first_donation.total_amount),
ONE_NEAR.as_yoctonear()
);
// Make another donation
set_context("donor_b", ONE_NEAR.saturating_mul(2));
contract.donate();
let second_donation = contract.get_donation_for_account("donor_b".parse().unwrap());
// Check the donation was recorded correctly
assert_eq!(
u128::from(second_donation.total_amount),
ONE_NEAR.saturating_mul(2).as_yoctonear()
);
// User A makes another donation on top of their original
set_context("donor_a", ONE_NEAR);
contract.donate();
let first_donation = contract.get_donation_for_account("donor_a".parse().unwrap());
// Check the donation was recorded correctly
assert_eq!(
u128::from(first_donation.total_amount),
ONE_NEAR.saturating_mul(2).as_yoctonear()
);
assert_eq!(u64::from(contract.number_of_donors()), 2);
}
// Auxiliar fn: create a mock context
fn set_context(predecessor: &str, amount: NearToken) {
let mut builder = VMContextBuilder::new();
builder.predecessor_account_id(predecessor.parse().unwrap());
builder.attached_deposit(amount);
testing_env!(builder.build());
}
[dev-dependencies]
near-sdk = { version = "5.29.0", features = ["unit-testing"] }
⚠️ Limitations
Unit tests are useful to check for code integrity, and detect basic errors on isolated methods. However, since unit tests do not run on a blockchain, there are many things which they cannot detect. Unit tests are not suitable for:
- Testing gas and storage usage
- Testing transfers
- Testing cross-contract calls
- Testing complex interactions, i.e. multiple users depositing money on the contract
For all these cases it is necessary to complement unit tests with integration tests.