Skip to main content

Integration Tests

Integration tests enable to deploy your contract in the NEAR testnet or a local sandbox, and create test-users to interact with it. In this way, you can thoroughly test your contract in a realistic environment.

Moreover, when using the local sandbox you gain complete control of the network:

  1. Create test Accounts and manipulate their State and Balance.
  2. Simulate errors on callbacks.
  3. Control the time-flow and fast-forward into the future (Rust ready, TS coming soon).
NEAR Workspaces

In NEAR, integration tests are implemented using a framework called Workspaces. Workspaces comes in two flavors: ๐Ÿฆ€ Rust and ๐ŸŒ Typescript.

All of our examples come with integration testing.


Snippet I: Testing Hello NEARโ€‹

Lets take a look at the test of our Quickstart Project ๐Ÿ‘‹ Hello NEAR, where we deploy the contract on an account and test it correctly retrieves and sets the greeting.


Snippet II: Testing Donationsโ€‹

In most cases we will want to test complex methods involving multiple users and money transfers. A perfect example for this is our Donation Example, which enables users to donate money to a beneficiary. Lets see its integration tests


Sandbox Testingโ€‹

NEAR Workspaces allows you to write tests once, and run them either on testnet or a local Sandbox. By default, Workspaces will start a sandbox and run your tests locally. Lets dive into the features of our framework and see how they can help you.

Spooning Contractsโ€‹

Spooning a blockchain is copying the data from one network into a different network. NEAR Workspaces makes it easy to copy data from Mainnet or Testnet contracts into your local Sandbox environment:

const refFinance = await root.importContract({
mainnetContract: 'v2.ref-finance.near',
blockId: 50_000_000,
withData: true,
});

This would copy the Wasm bytes and contract state from v2.ref-finance.near to your local blockchain as it existed at block 50_000_000. This makes use of Sandbox's special patch state feature to keep the contract name the same, even though the top level account might not exist locally (note that this means it only works in Sandbox testing mode). You can then interact with the contract in a deterministic way the same way you interact with all other accounts created with near-workspaces.

note

withData will only work out-of-the-box if the contract's data is 50kB or less. This is due to the default configuration of RPC servers; see the "Heads Up" note here.

See a TypeScript example of spooning contracts.

Patch State on the Flyโ€‹

In Sandbox-mode, you can add or modify any contract state, contract code, account or access key with patchState.

tip

You can alter contract code, accounts, and access keys using normal transactions via the DeployContract, CreateAccount, and AddKey actions. But this limits you to altering your own account or sub-account. patchState allows you to perform these operations on any account.

Keep in mind that you cannot perform arbitrary mutation on contract state with transactions since transactions can only include contract calls that mutate state in a contract-programmed way. For example, with an NFT contract, you can perform some operation with NFTs you have ownership of, but you cannot manipulate NFTs that are owned by other accounts since the smart contract is coded with checks to reject that. This is the expected behavior of the NFT contract. However, you may want to change another person's NFT for a test setup. This is called "arbitrary mutation on contract state" and can be done with patchState:

    const {contract, ali} = t.context.accounts;
// Contract must have some state for viewState & patchState to work
await ali.call(contract, 'set_status', {message: 'hello'});
// Get state
const state = await contract.viewState();
// Get raw value
const statusMessage = state.get('STATE', {schema, type: StatusMessage});
// Update contract state
statusMessage.records.push(
new BorshRecord({k: 'alice.near', v: 'hello world'}),
);
// Serialize and patch state back to runtime
await contract.patchState(
'STATE',
borsh.serialize(schema, statusMessage),
);
// Check again that the update worked
const result = await contract.view('get_status', {
account_id: 'alice.near',
});
t.is(result, 'hello world');

To see a complete example of how to do this, see the patch-state test.

As an alternative to patchState, you can stop the node, dump state at genesis, edit the genesis, and restart the node. This approach is more complex to do and also cannot be performed without restarting the node.

Time Travelingโ€‹

workspaces offers support for forwarding the state of the blockchain to the future. This means contracts which require time sensitive data do not need to sit and wait the same amount of time for blocks on the sandbox to be produced. We can simply just call worker.fast_forward to get us further in time:


Using Testnetโ€‹

NEAR Workspaces is set up so that you can write tests once and run them against a local Sandbox node (the default behavior) or against NEAR TestNet. Some reasons this might be helpful:

  • Gives higher confidence that your contracts work as expected
  • You can test against deployed testnet contracts
  • If something seems off in Sandbox mode, you can compare it to testnet

You can switch to testnet mode in three ways:

1. Setting the Worker network to testnet

When creating Worker set network to testnet and pass your master account:

const worker = await Worker.init({
network: 'testnet',
testnetMasterAccountId: '<yourAccountName>',
initialBalance: NEAR.parse("<X> N").toString(),
})
2. Setting environment variables

Set the NEAR_WORKSPACES_NETWORK and TESTNET_MASTER_ACCOUNT_ID environment variables when running your tests:

NEAR_WORKSPACES_NETWORK=testnet TESTNET_MASTER_ACCOUNT_ID=<your master account Id> node test.js

If you set this environment variables and pass {network: 'testnet', testnetMasterAccountId: <masterAccountId>} to Worker.init, the config object takes precedence.

3. Config file

If you are using AVA, you can use a custom config file. Other test runners allow similar config files; adjust the following instructions for your situation.

Create a file in the same directory as your package.json called ava.testnet.config.cjs with the following contents:

module.exports = {
...require('near-workspaces/ava.testnet.config.cjs'),
...require('./ava.config.cjs'),
};
module.exports.environmentVariables = {
TESTNET_MASTER_ACCOUNT_ID: '<masterAccountId>',
};

The near-workspaces/ava.testnet.config.cjs import sets the NEAR_WORKSPACES_NETWORK environment variable for you. A benefit of this approach is that you can then easily ignore files that should only run in Sandbox mode.

Now you'll also want to add a test:testnet script to your package.json's scripts section:

"scripts": {
"test": "ava",
+ "test:testnet": "ava --config ./ava.testnet.config.cjs"
}

To use the accounts, you will need to create the .near-credentials/workspaces/testnet directory and add files for your master account, for example:

// .near-credentials/workspaces/testnet/<your-account>.testnet.json 
{"account_id":"<your-account>.testnet","public_key":"ed25519:...","private_key":"ed25519:..."}

Additional Mediaโ€‹

Test Driven Design Using Workspaces and AVAโ€‹

The video below walks through how to apply TDD with Workspaces and AVA for a simple contract:

Was this page helpful?