Answer by example:
Suppose you have a function that can be invoked only by the owner (deployer) of the contract.
If it's invoked by anyone other than the owner, then it reverts (require
with a false
condition).
File exceptions.js
:
module.exports.errTypes = {
revert : "revert",
outOfGas : "out of gas",
invalidJump : "invalid JUMP",
invalidOpcode : "invalid opcode",
stackOverflow : "stack overflow",
stackUnderflow : "stack underflow",
staticStateChange : "static state change"
}
module.exports.tryCatch = async function(promise, errType) {
try {
await promise;
throw null;
}
catch (error) {
assert(error, "Expected an error but did not get one");
assert(error.message.startsWith(PREFIX + errType), "Expected an error starting with '" + PREFIX + errType + "' but got '" + error.message + "' instead");
}
};
const PREFIX = "VM Exception while processing transaction: ";
File MyContractTest.js
:
contract("MyContractTest", function(accounts) {
let myContract;
let owner = accounts[0];
let nonOwner = accounts[1];
let tryCatch = require("./exceptions.js").tryCatch;
let errTypes = require("./exceptions.js").errTypes;
describe("sanity assertion:", function() {
before(async function() {
myContract = await artifacts.require("MyContract.sol").new();
});
it("should complete successfully", async function() {
await myContract.func({from: owner});
});
it("should abort with an error", async function() {
await tryCatch(myContract.func({from: nonOwner}), errTypes.revert);
});
});
});
UPDATE: Version 2
I would like to update this answer with slightly simpler code. Its behavior should be identical, but it "saves a few characters" in the test itself, thus improves readability and allows easier maintenance.
File exceptions.js
:
const PREFIX = "Returned error: VM Exception while processing transaction: ";
async function tryCatch(promise, message) {
try {
await promise;
throw null;
}
catch (error) {
assert(error, "Expected an error but did not get one");
assert(error.message.startsWith(PREFIX + message), "Expected an error starting with '" + PREFIX + message + "' but got '" + error.message + "' instead");
}
};
module.exports = {
catchRevert : async function(promise) {await tryCatch(promise, "revert" );},
catchOutOfGas : async function(promise) {await tryCatch(promise, "out of gas" );},
catchInvalidJump : async function(promise) {await tryCatch(promise, "invalid JUMP" );},
catchInvalidOpcode : async function(promise) {await tryCatch(promise, "invalid opcode" );},
catchStackOverflow : async function(promise) {await tryCatch(promise, "stack overflow" );},
catchStackUnderflow : async function(promise) {await tryCatch(promise, "stack underflow" );},
catchStaticStateChange : async function(promise) {await tryCatch(promise, "static state change");},
};
File MyContractTest.js
:
contract("MyContractTest", function(accounts) {
let myContract;
let owner = accounts[0];
let nonOwner = accounts[1];
let catchRevert = require("./exceptions.js").catchRevert;
describe("sanity assertion:", function() {
before(async function() {
myContract = await artifacts.require("MyContract.sol").new();
});
it("should complete successfully", async function() {
await myContract.func({from: owner});
});
it("should abort with an error", async function() {
await catchRevert(myContract.func({from: nonOwner}));
});
});
});