How to test solidity require in JavaScript

Smart contracts

Testing is one of the most important measures for improving the security of smart contracts. Unlike traditional software, once smart contracts are deployed on the blockchain, they are immutable. They cannot be updated, making it much more essential to test rigorously before deploying.

We assume that you know the basics of smart contracts, solidity, and testing using mocha.

Example

Now let us start with an example. Below is a simple storage smart contract with two storage variables: _data and owner.

  • _data: This is a private unit storage variable.
  • _owner: This is a private address storage variable to store the address of the person who deploys the contract.

We've got a constructor function to set the _owner to the contract caller that is the msg.sender. The setData(uint) and getData()are self-explanatory and are used to set and get the value of the private variable _data respectively.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint private _data;
address private _owner;
constructor() {
_owner = msg.sender;
}
function setData(uint data_) external {
_data = data_;
}
function getData() external view returns (uint) {
return _data;
}
}
Create a SimpleStorage contract

Below is a simple Mocha test to test the smart contract. We first set the value of _data to 10. Later, we make use of the getData() function to retrieve its value. For our test to pass, these two values must be equal.

const SimpleStorage = artifacts.require("SimpleStorage");
contract("Simple Storage", accounts => {
let instance = null;
before(async () => {
instance = await SimpleStorage.deployed()
});
it("should set the data", async () => {
await instance.setData(10);
let result = await instance.getData().toNumber();
assert.equal(result, 10);
});
})
Test the SimpleStorage contract

This was quite simple!

We'll find many tutorials and examples like this if we search for testing with solidity. But, in real-world smart contacts are not as simple as this one. An average smart contract is composed of different solidity error handling constructs like require , revert and assert. We'll explain how to test while encountering require statements, but the method is the same for the other two as well.

Let us extend our smart contract and make it so that only the _owner can set the value of _data . It is quite simple. We just need to need a single require statement.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleStorage {
uint private _data;
address private _owner;
constructor() {
_owner = msg.sender;
}
function setData(uint data_) external {
require(
msg.sender == _owner,
"SimpleStorage: Only owner is allowed"
)
_data = data_;
}
function getData() external view returns (uint) {
return _data;
}
}
Add require to the contract

Below is the updated test. We have just changed line 10. We are calling the setData() using the second account from the accounts array. If we don't pass the account, the first account is used for transactions by default. In this case, the first account is the _owner . Now, what would happen if we execute this code? Think!

const SimpleStorage = artifacts.require("SimpleStorage");
contract("Simple Storage", accounts => {
let instance = null;
before(async () => {
instance = await SimpleStorage.deployed()
});
it("should not set the data", async () => {
await instance.setData(10, {from: accounts[1]});
let result = await instance.getData().toNumber();
assert.equal(result, 10);
});
})
Test require Statement, The WRONG way

You probably guessed it right!

It would throw an error because the solidity function throws an error due to the failing require statement. So, what is the solution?

It is quite simple! We just need to wrap the call inside a try/catch block.

const SimpleStorage = artifacts.require("SimpleStorage");
contract("Simple Storage", accounts => {
let instance = null;
before(async () => {
instance = await SimpleStorage.deployed()
});
it("should not set the data", async () => {
try {
await instance.setData(10, {from: accounts[1]});
assert.fail("The transaction should have thrown an error");
} catch(err) {
assert.include(err.message, "revert", "The error message should contain 'revert'");
}
});
})
Test require Statement, The RIGHT way

Explanation

  • Line 12: If the code execution reaches this statement, this means that our require did not trigger, and there is something wrong with our logic.
  • Line 13: We'll catch the error.
  • Line 14: Checks if the error contains the revert keyword. This is to make sure that the error is thrown by the revert statement and not anything else. Check for the custom error message too.

Free Resources