Chai matchers

A set of sweet chai matchers, makes your test easy to write and read. Before you can start using the matchers, you have to tell chai to use the solidity plugin:

import chai from "chai";
import { solidity } from "ethereum-waffle";

chai.use(solidity);

Below is the list of available matchers:

Bignumbers

Testing equality of big numbers:

expect(await token.balanceOf(wallet.address)).to.equal(993);

Available matchers for BigNumbers are: equal, eq, above, gt, gte, below, lt, lte, least, most, within, closeTo.

expect(BigNumber.from(100)).to.be.within(BigNumber.from(99), BigNumber.from(101));
expect(BigNumber.from(100)).to.be.closeTo(BigNumber.from(101), 10);

Emitting events

Testing what events were emitted with what arguments:

Note

You must await the expect in order for the matcher to execute properly. Failing to await will cause the assertion to pass whether or not the event is actually emitted!

await expect(token.transfer(walletTo.address, 7))
  .to.emit(token, 'Transfer')
  .withArgs(wallet.address, walletTo.address, 7);

Note

The matcher will match indexed event parameters of type string or bytes even if the expected argument is not hashed using keccack256 first.

Testing with indexed bytes or string parameters. These two examples are equivalent

await expect(contract.addAddress("street", "city"))
  .to.emit(contract, 'AddAddress')
  .withArgs("street", "city");

const hashedStreet = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("street"));
const hashedCity = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("city"));
await expect(contract.addAddress(street, city))
  .to.emit(contract, 'AddAddress')
  .withArgs(hashedStreet, hashedCity);

If you are using Waffle version 3.4.4 or lower, you can’t chain emit matchers like in example below.

await expect(tx)
  .to.emit(contract, 'One')
  .to.emit(contract, 'Two');

This feature is available in Waffle version 4.

Testing the argument names in the event:

await expect(token.transfer(walletTo.address, 7))
  .to.emit(token, 'Transfer')
  .withNamedArgs({
    from: wallet.address,
    to: walletTo.address,
    amount: 7
  });

A subset of arguments in an event can be tested by only including the desired arguments:

await expect(token.transfer(walletTo.address, "8000000000000000000"))
  .to.emit(token, 'Transfer')
  .withNamedArgs({
    amount: "8000000000000000000"
  });

This feature will be available in Waffle version 4.

Events declared and emitted in a library:

If your contract is using a Solidity Library that declares and emits events and you want to test your contract for emitting those events, you’ll have to specifically point to the library. You can use attach method on the library object. An example snippet for this case:

await expect(contract.function())
  .to.emit(library.attach(contract.address), 'MyEvent');

where library is the ethers object of the Solidity Library in which MyEvent is declared.

Called on contract

Testing if function was called on the provided contract:

await token.balanceOf(wallet.address)

expect('balanceOf').to.be.calledOnContract(token);

Called on contract with arguments

Testing if function with certain arguments was called on provided contract:

await token.balanceOf(wallet.address)

expect('balanceOf').to.be.calledOnContractWith(token, [wallet.address]);

Revert

Testing if transaction was reverted:

await expect(token.transfer(walletTo.address, 1007)).to.be.reverted;

Revert with message

Testing if transaction was reverted with certain message:

await expect(token.transfer(walletTo.address, 1007))
  .to.be.revertedWith('Insufficient funds');

You can also test if revert message matches to a regular expression:

await expect(token.checkRole('ADMIN'))
  .to.be.revertedWith(/AccessControl: account .* is missing role .*/);

Change ether balance

Testing whether the transaction changes the balance of the account:

await expect(() => wallet.sendTransaction({to: walletTo.address, value: 200}))
  .to.changeEtherBalance(walletTo, 200);

await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
  .to.changeEtherBalance(walletTo, 200);

expect for changeEtherBalance gets one of the following parameters:

Note

changeEtherBalance won’t work if there is more than one transaction mined in the block.

The transaction call should be passed to the expect as a callback (we need to check the balance before the call) or as a transaction response.

The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.

changeEtherBalance ignores transaction fees by default:

// Default behavior
await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
  .to.changeEtherBalance(wallet, -200);

// To include the transaction fee use:
await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 1, value: 200}))
  .to.changeEtherBalance(wallet, -21200, {includeFee: true});

Note

changeEtherBalance calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeEtherBalances matcher.

Change ether balance (multiple accounts)

Testing whether the transaction changes balance of multiple accounts:

await expect(() => wallet.sendTransaction({to: walletTo.address, value: 200}))
  .to.changeEtherBalances([wallet, walletTo], [-200, 200]);

await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
  .to.changeEtherBalances([wallet, walletTo], [-200, 200]);

Note

changeEtherBalances calls won’t work if there is more than one transaction mined in the block.

Change token balance

Testing whether the transfer changes the balance of the account:

await expect(() => token.transfer(walletTo.address, 200))
  .to.changeTokenBalance(token, walletTo, 200);

await expect(() => token.transferFrom(wallet.address, walletTo.address, 200))
  .to.changeTokenBalance(token, walletTo, 200);

Note

The transfer call should be passed to the expect as a callback (we need to check the balance before the call).

The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.

Note

changeTokenBalance calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeTokenBalances matcher.

Change token balance (multiple accounts)

Testing whether the transfer changes balance for multiple accounts:

await expect(() => token.transfer(walletTo.address, 200))
  .to.changeTokenBalances(token, [wallet, walletTo], [-200, 200]);

Proper address

Testing if a string is a proper address:

expect('0x28FAA621c3348823D6c6548981a19716bcDc740e').to.be.properAddress;

Proper private key

Testing if a string is a proper private key:

expect('0x706618637b8ca922f6290ce1ecd4c31247e9ab75cf0530a0ac95c0332173d7c5').to.be.properPrivateKey;

Proper hex

Testing if a string is a proper hex value of given length:

expect('0x70').to.be.properHex(2);

Hex Equal

Testing if a string is a proper hex with value equal to the given hex value. Case insensitive and strips leading zeros:

expect('0x00012AB').to.hexEqual('0x12ab');

Deprecated matchers

Change balance

Deprecated since version 3.1.2: Use changeEtherBalance() instead.

Testing whether the transaction changes the balance of the account:

await expect(() => wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
  .to.changeBalance(walletTo, 200);

await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
  .to.changeBalance(walletTo, 200);

expect for changeBalance gets one of the following parameters:

Note

changeBalance won’t work if there is more than one transaction mined in the block.

The transaction call should be passed to the expect as a callback (we need to check the balance before the call) or as a transaction response.

The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.

Note

changeBalance calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeBalances matcher.

Change balance (multiple accounts)

Deprecated since version 3.1.2: Use changeEtherBalances() instead.

Testing whether the transaction changes balance of multiple accounts:

await expect(() => wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
  .to.changeBalances([wallet, walletTo], [-200, 200]);

await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
  .to.changeBalances([wallet, walletTo], [-200, 200]);

Note

changeBalances calls won’t work if there is more than one transaction mined in the block.