Note: simulation tests are being deprecated in favor of the Sandbox. Until the Sandbox is ready, simulation tests are still a useful approach to the types of testing described on this page. Please note, however, that there will not be ongoing development for it.
Unit tests are great for ensuring that functionality works as expected. This might include checking that function
get_nth_fibonacci(n: u8) works as expected, handles invalid input gracefully, etc. Unit tests in smart contracts might similarly test public functions, but can get unruly if there are several calls between accounts. As mentioned in the unit tests section, there is a
VMContext object used by unit tests to mock some aspects of a transaction. One might, for instance, modify the testing context to have the
"bob.near". The limits of unit tests become obvious with certain interactions, like transferring tokens. Since
"bob.near" is simply a string and not an account object, there is no way to write a unit test that confirms that Alice sent Bob 6 NEAR (Ⓝ). Furthermore, there is no way to write a unit test that executes cross-contract calls.
Simulation tests provide the ability to have (mocked) end-to-end testing that includes cross-contract calls, proper user accounts, access to state, structured execution outcomes, and more. It should be noted that simulation tests are not a one-to-one match with the behavior of nodes, but still offers significantly improved features to unit tests. (The Sandbox aims to offer the same behavior as real nodes, with the ability to patch state and more.)
You'll probably want to use simulation tests when:
- there are cross-contract calls
- there are multiple contracts that interact
- there are multiple users whose balance changes
- you're writing something that "feels" like an end-to-end test
Unlike unit tests (which often live in the
src/lib.rs file of the contract), simulation tests are conventionally in a separate subdirectory of the contract called
tests. Refer to this folder structure below:
Cargo.toml file will need to have two items to run simulation tests.
- The library target's
rlib. See the Rust docs for more information about the manifest's library target. See the example below:
main.rs file above will contain the simulation tests. These will be run alongside the other tests using the typical test command:
You may also run the simpler command
cargo test, but the one above will allow for messages to appear on the terminal, such as logs and print lines. More info here.
Let's take a look at a very simple unit test and simulation test that accomplish the same thing. Normally you wouldn't duplicate efforts like this, but it will be informative.
First, note this unit test that tests the functionality of the
The test above sets up the testing context, instantiates the smart contract's struct called
Counter, calls the
increment method, and makes an assertion that a number field on the struct is now
Let's look at how this might be written with simulation tests. The snippet below is a bit longer as it demonstrates a couple of things worth noting.
In the simulation above, we initialize the simulator, getting a
root account. Then the compiled smart contract for the Rust Counter example is deployed to a simulation environment. This environment does not persist between testing and offers a similar environment to how a NEAR network node interacts. Next we see calls to a view and change method (with the
call! macros) along with an assertion checking the expected value.
Above halfway down the simulation test we'll see another approach to a view and change method that doesn't use macros. (These begin with
root.view.) The choice between using the macro or non-macro approach is stylistic. The fungible token example is a great reference for the macro and non-macro approaches to testing.
Please visit the crate reference documentation for details on the available methods with simulation testing.
Pitfall: you must compile your contract before running simulation tests. Because sim tests use the Wasm binary, if changes are made to the smart contract code it will not automatically be tested against.
Quirk: behind the scenes, simulation tests will create an import object using Pascal casing. The format is StructnameContract. A number of smart contracts call their primary struct (the one decorated with #[near_bindgen])
Contract. Hence the Pascal case object becomes ContractContract.
Contracts like this will often assign a different name as shown below:
We'll finish this page with some helpful examples demonstrating various aspects of simulation testing.
This sets the variable
res to be the execution result of a change method. It also demonstrates unwrapping a value from the call. Note that besides
unwrap_json there is also
unwrap_json_value which may be useful.
A helpful utility function to check if an execution result has failed. This does not give insight into the error message itself.
While not ideal (as it looks for a substring) this is a method to identify a particular error that's thrown.
This example (and particularly the entire test) shows how one might produce blocks using simulation tests. This may be useful if a contract is supposed to act differently based on block height. As mentioned earlier in this page, simulation tests will be deprecated, so the syntax may be a bit odd for this use case.