Skip to content
by Unity Nodes
docs.near.orgweb3 apps › tutorials › mastering near › 1.2 testing

Sandbox Testing

Updated 01-11-2026

Lets test our contract in a realistic sandbox environment.

In the previous section, we went through the contract's code, analyzing how it worked. Now, we need to test it and make sure it works as expected! For contracts, there are two types of testing you can do: unit testing and sandbox testing.

Here, we will focus on sandbox testing, as it enables one to deploy the contract in a realistic environment, allowing us to create multiple accounts and interact with the contract as if it was deployed on the blockchain.

[!NOTE]

unit testing

Unit tests are built into the language and are used to test the contract functions individually. These tests work well when little context is required. However, they cannot test chain interactions - like sending accounts $NEAR tokens - since they need to be processed by the network.


We use near-sandbox to start a local blockchain and near-api to deploy contracts, submit transactions, and read state. These tests run locally without a testnet account.

Account Creation

The first thing our test does is to create multiple accounts with 10 $NEAR tokens each and deploy the contract to one of them.

// Build the contract wasm file
let contract_wasm_path = cargo_near_build::build_with_cli(Default::default())?;
let contract_wasm = std::fs::read(contract_wasm_path)?;

// Initialize the sandbox
let sandbox = near_sandbox::Sandbox::start_sandbox().await?;
let sandbox_network =
    near_api::NetworkConfig::from_rpc_url("sandbox", sandbox.rpc_addr.parse()?);

// Create accounts
let alice = create_subaccount(&sandbox, "alice.sandbox").await?;
let bob = create_subaccount(&sandbox, "bob.sandbox").await?;
let auctioneer = create_subaccount(&sandbox, "auctioneer.sandbox").await?;
let contract = create_subaccount(&sandbox, "contract.sandbox")
    .await?
    .as_contract();

// Initialize signer for the contract deployment
let signer = near_api::Signer::from_secret_key(
    near_sandbox::config::DEFAULT_GENESIS_ACCOUNT_PRIVATE_KEY
        .parse()
        .unwrap(),
)?;

See code on GitHub

The test uses cargo_near_build to compile the contract before deploying it to the sandbox, so we do not need to pre-compile it ourselves.


Contract Initialization

The test reads the sandbox's block timestamp and sets end_time to 60 seconds later. It then deploys the contract and calls init in the same transaction.

// Calculate the end time for the auction as a parameter for the init function
let now = near_api::Chain::block()
    .fetch_from(&sandbox_network)
    .await?
    .header
    .timestamp_nanosec
    .parse::<u64>()?;
let a_minute_from_now = now + 60_000_000_000;

// Deploy the contract with the init call
near_api::Contract::deploy(contract.account_id().clone())
    .use_code(contract_wasm)
    .with_init_call(
        "init",
        json!({"end_time": a_minute_from_now.to_string(), "auctioneer": auctioneer.account_id()}),
    )?
    .with_signer(signer.clone())
    .send_to(&sandbox_network)
    .await?
    .assert_success();

See code on GitHub

[!WARNING]

Time Units

The contract measures time in nanoseconds. The sandbox block timestamp is already in nanoseconds, so we add 60_000_000_000 to set a deadline 60 seconds later.

[!NOTE]

Time is a String

The U64 timestamp is passed as a String to preserve all 64 bits when serialized as JSON.


Bidding

Now that the contract is deployed and initialized, we can start bidding and checking if the contract behaves as expected.

We first make alice place a bid of 1 NEAR, and check that the contract correctly registers the bid. Then, we have bob place a bid of 2 NEAR, and check that the highest bid is updated, and that alice gets her NEAR refunded.

// Alice makes first bid
contract
    .call_function("bid", ())
    .transaction()
    .deposit(NearToken::from_near(1))
    .with_signer(alice.account_id().clone(), signer.clone())
    .send_to(&sandbox_network)
    .await?
    .assert_success();

// For now, the highest bid is the Alice's bid
let highest_bid: Bid = contract
    .call_function("get_highest_bid", ())
    .read_only()
    .fetch_from(&sandbox_network)
    .await?
    .data;
assert_eq!(highest_bid.bid, NearToken::from_near(1));
assert_eq!(&highest_bid.bidder, alice.account_id());

let alice_balance = alice
    .tokens()
    .near_balance()
    .fetch_from(&sandbox_network)
    .await?
    .total;

// Now, Bob makes a higher bid
contract
    .call_function("bid", ())
    .transaction()
    .deposit(NearToken::from_near(2))
    .with_signer(bob.account_id().clone(), signer.clone())
    .send_to(&sandbox_network)
    .await?
    .assert_success();

// Now, the highest bid is the Bob's bid
let highest_bid: Bid = contract
    .call_function("get_highest_bid", ())
    .read_only()
    .fetch_from(&sandbox_network)
    .await?
    .data;
assert_eq!(highest_bid.bid, NearToken::from_near(2));
assert_eq!(&highest_bid.bidder, bob.account_id());

// Check that Alice was refunded her bid
let new_alice_balance = alice
    .tokens()
    .near_balance()
    .fetch_from(&sandbox_network)
    .await?
    .total;
assert!(new_alice_balance == alice_balance.saturating_add(NearToken::from_near(1)));

See code on GitHub

Checking the balance

It is important to notice how we check if alice was refunded. We query her balance after her first bid, and then check if it has increased by 1 NEAR after bob makes his bid.

You might be tempted to check if alice's balance is exactly 10 NEAR after she gets refunded, but alice balance cannot be 10 NEAR anymore, because some $NEAR was consumed as gas fees when alice called bid.

Testing invalid calls

When testing we should also check that the contract does not allow invalid calls. The next part checks that the contract doesn't allow for bids with fewer $NEAR tokens than the previous to be made.

// Alice tries to make a bid with less NEAR than the previous
contract
    .call_function("bid", ())
    .transaction()
    .deposit(NearToken::from_near(1))
    .with_signer(alice.account_id().clone(), signer.clone())
    .send_to(&sandbox_network)
    .await?
    .assert_failure();

See code on GitHub


Fast Forwarding Time

The sandbox allows us to fast-forward time, which is useful for testing the contract when the auction is over. The test advances 200 blocks in order to pass a minute, and thus allowing the auction to be claimed.

After which the auction can now be claimed. Once claimed the test checks that the auctioneer has received the correct amount of $NEAR tokens.

// Fast forward 200 blocks
let blocks_to_advance = 200;
sandbox.fast_forward(blocks_to_advance).await?;

// Auctioneer claims the auction
contract
    .call_function("claim", ())
    .transaction()
    .gas(NearGas::from_tgas(30))
    .with_signer(auctioneer.account_id().clone(), signer.clone())
    .send_to(&sandbox_network)
    .await?
    .assert_success();

// Checks the auctioneer has the correct balance
let auctioneer_balance = auctioneer
    .tokens()
    .near_balance()
    .fetch_from(&sandbox_network)
    .await?
    .total;
assert!(auctioneer_balance <= NearToken::from_near(12));
assert!(auctioneer_balance > NearToken::from_millinear(11990));

See code on GitHub

If you review the tests in full you'll see that we also test other invalid calls such as the auctioneer trying to claim the auction before it is over and a user attempting to bid once the auction is over.


Executing the tests

Now that we understand what we are testing, let's go ahead and run the tests!

cargo test --locked

The first run downloads Rust dependencies and the sandbox binary. All tests should pass, and you should see the output of the tests in the console. If you see any errors, please contact us in the NEAR Discord or through Telegram and we'll help you out!


Conclusion

In this part of the tutorial, we've seen how to use our sandbox testing environment to test the contract. We've tested the contract's initialization, bidding, and time advancement.

You are now ready to move to the next section, where we will deploy the contract to testnet and interact with it through the CLI.