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:
transaction call : () => Promise<TransactionResponse>
transaction response : TransactionResponse
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:
transaction call : () => Promise<TransactionResponse>
transaction response : TransactionResponse
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.