Skip to content

Commit 575239c

Browse files
authored
Merge pull request #2793 from NomicFoundation/gene/hh-696
[docs] chai matchers & network helpers: what are they?
2 parents f6a5be1 + 980f3a4 commit 575239c

File tree

7 files changed

+344
-8
lines changed

7 files changed

+344
-8
lines changed
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
section-type: hidden
1+
section-type: group
2+
section-title: Hardhat Chai Matchers
3+
order:
4+
- href: /
5+
title: What is it?
6+
- /migrate-from-waffle
7+
- /reference
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Chai Matchers <!-- An Explanation: theoretical, not practical; for studying, not for working-->
2+
3+
<!-- What is it? -->
4+
5+
[@nomicfoundation/hardhat-chai-matchers](https://www.npmjs.com/package/@nomicfoundation/hardhat-chai-matchers) adds Ethereum-specific capabilities to the [Chai](https://www.chaijs.com/) assertion library, making your smart contract tests easy to write and read.
6+
7+
Among other things, you can assert that a contract fired certain events, or that it exhibited a specific revert, or that a transaction resulted in specific changes to a wallet's Ether or token balance.
8+
9+
## How can I use it?
10+
11+
Simply `require("@nomicfoundation/hardhat-chai-matchers")` in your Hardhat config and then the assertions will be available in your code.
12+
13+
A few other helpers, such as argument predicates and panic code constants, must be imported explicitly. These are discussed below.
14+
15+
## Why would I want to use it?
16+
17+
### Events
18+
19+
You can easily write tests to verify that your contract emitted a certain event. For example, `await expect(contract.call()).to.emit(contract, "Event")` would detect the event emitted by the following Solidtity code:
20+
21+
```solidity
22+
contract C {
23+
event Event();
24+
function call () public {
25+
emit Event();
26+
}
27+
}
28+
```
29+
30+
Note that the `await` is required before an `expect(...).to.emit(...)`, because the verification requires the retrieval of the event logs from the Ethereum node, which is an asynchronous operation. Without that initial `await`, your test may run to completion before the Ethereum transaction even completes.
31+
32+
Also note that the first argument to `emit()` is the contract which emits the event. If your contract calls another contract, and you want to detect an event from the inner contract, you need to pass in the inner contract to `emit()`.
33+
34+
#### Events with Arguments
35+
36+
Solidity events can contain arguments, and you can assert the presence of certain argument values in an event that was emitted. For example, to assert that an event emits a certain unsigned integer value:
37+
38+
<!-- prettier-ignore -->
39+
```js
40+
await expect(contract.call())
41+
.to.emit(contract, "Uint")
42+
.withArgs(3);
43+
```
44+
45+
Sometimes you may want to assert the value of the second argument of an event, but you want to permit any value for the first argument. This is easy with `withArgs` because it supports not just specific values but also _predicates_. For example, to skip checking the first argument but assert the value of the second:
46+
47+
```js
48+
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
49+
await expect(contract.call())
50+
.to.emit(contract, "TwoUints")
51+
.withArgs(anyValue, 3);
52+
```
53+
54+
Predicates are simply functions that, when called, indicate whether the value should be considered successfully matched or not. The function will receive the value as its input, but it need not use it. For example, the `anyValue` predicate is simply `() => true`.
55+
56+
This package provides the predicates `anyValue` and `anyUint`, but you can easily create your own:
57+
58+
<!-- prettier-ignore -->
59+
```js
60+
function isEven(x: BigNumber): boolean {
61+
return x.mod(2).isZero();
62+
}
63+
64+
await expect(contract.emitUint(2))
65+
.to.emit(contract, "Uint")
66+
.withArgs(isEven);
67+
```
68+
69+
### Reverts
70+
71+
You can also easily write tests that assert whether a contract call reverted (or not) and what sort of error data to expect along with the revert.
72+
73+
The most simple case asserts that a revert happened:
74+
75+
```js
76+
await expect(contract.call()).to.be.reverted;
77+
```
78+
79+
Or, conversely, that one didn't:
80+
81+
```js
82+
await expect(contract.call()).not.to.be.reverted;
83+
```
84+
85+
A revert may also include some error data, such as a string, a [panic code](https://docs.soliditylang.org/en/v0.8.14/control-structures.html#panic-via-assert-and-error-via-require), or a [custom error](https://docs.soliditylang.org/en/v0.8.14/contracts.html#errors-and-the-revert-statement), and this package provides matchers for all of them.
86+
87+
The `revertedWith` matcher allows you to assert that a revert's error data does or doesn't match a specific string:
88+
89+
```js
90+
await expect(contract.call()).to.be.revertedWith("Some revert message");
91+
await expect(contract.call()).not.to.be.revertedWith("Another revert message");
92+
```
93+
94+
The `revertedWithPanic` matcher allows you to assert that a revert did or didn't occurr with a specific [panic code](https://docs.soliditylang.org/en/v0.8.14/control-structures.html#panic-via-assert-and-error-via-require). You can match a panic code via its integer value (including via hexadecimal notation, such as `0x12`) or via the `PANIC_CODES` dictionary exported from this package:
95+
96+
```js
97+
const { PANIC_CODES } = require("@nomicfoundation/hardhat-chai-matchers/panic");
98+
99+
await expect(contract.divideBy(0)).to.be.revertedWithPanic(
100+
PANIC_CODES.DIVISION_BY_ZERO
101+
);
102+
103+
await expect(contract.divideBy(1)).not.to.be.revertedWithPanic(
104+
PANIC_CODES.DIVISION_BY_ZERO
105+
);
106+
```
107+
108+
You can omit the panic code in order to assert that the transaction reverted with _any_ panic code.
109+
110+
The `revertedWithCustomError` matcher allows you to assert that a transaction reverted with a specific [custom error](https://docs.soliditylang.org/en/v0.8.14/contracts.html#errors-and-the-revert-statement):
111+
112+
```js
113+
await expect(contract.call()).to.be.revertedWithCustomError(
114+
contract,
115+
"SomeCustomError"
116+
);
117+
```
118+
119+
Just as with events, the first argument to this matcher must specify the contract that defines the custom error. If you're expecting an error from a nested call to a different contract, then you'll need to pass that different contract as the first argument.
120+
121+
Further, just as events can have arguments, so too can custom error objects, and, just as with events, you can assert the values of these arguments. To do this, use the same `.withArgs()` matcher, and the same predicate system:
122+
123+
```js
124+
await expect(contract.call())
125+
.to.be.revertedWithCustomError(contract, "SomeCustomError")
126+
.withArgs(anyValue, "some error data string");
127+
```
128+
129+
Finally, you can assert that a call reverted without any error data (neither a reason string, nor a panic code, nor a custom error):
130+
131+
```js
132+
await expect(contract.call()).to.be.revertedWithoutReason();
133+
```
134+
135+
### Big Numbers
136+
137+
Working with Ethereum smart contracts in JavaScript can be annoying due Ethereum's 256-bit native integer size. Contracts returning integer values can yield numbers greater than JavaScript's maximum safe integer value, and writing assertions about the expectations of such values can be difficult without prior familiarity with the 3rd-party big integer library used by your web3 framework.
138+
139+
This package enhances the standard numerical equality matchers (`equal`, `above`, `within`, etc) such that you can seamlessly mix and match contract return values with regular `Number`s. For example:
140+
141+
```js
142+
expect(await token.balanceOf(someAddress)).to.equal(1);
143+
```
144+
145+
These matchers support not just [ethers' `BigNumber`](https://docs.ethers.io/v5/single-page/#/v5/api/utils/bignumber/) and the native JavaScript `Number`, but also [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt), [bn.js](https://github.com/indutny/bn.js/), and [bignumber.js](https://github.com/MikeMcl/bignumber.js/).
146+
147+
### Balance Changes
148+
149+
Often times, a transaction you're testing will be expected to have some effect on a wallet's balance, either its balance of Ether or its balance of some ERC-20 token. Another set of matchers allows you to verify that a transaction resulted in such a balance change:
150+
151+
```js
152+
await expect(() =>
153+
sender.sendTransaction({ to: someAddress, value: 200 })
154+
).to.changeEtherBalance(sender, "-200");
155+
156+
await expect(token.transfer(account, 1)).to.changeTokenBalance(
157+
token,
158+
account,
159+
1
160+
);
161+
```
162+
163+
Further, you can also check these conditions for multiple addresses at the same time:
164+
165+
```js
166+
await expect(() =>
167+
sender.sendTransaction({ to: receiver, value: 200 })
168+
).to.changeEtherBalances([sender, receiver], [-200, 200]);
169+
170+
await expect(token.transferFrom(sender, receiver, 1)).to.changeTokenBalances(
171+
[sender, receiver],
172+
[-1, 1]
173+
);
174+
```
175+
176+
### Miscellaneous String Checks
177+
178+
Sometimes you may also need to verify that hexadecimal string data is appropriate for the context it's used in. A handful of other matchers help you with this:
179+
180+
The `properHex` matcher asserts that the given string consists only of valid hexadecimal characters and that its length (the number of hexadecimal digits) matches its second argument:
181+
182+
```js
183+
expect("0x1234").to.be.properHex(4);
184+
```
185+
186+
The `properAddress` matcher asserts that the given string is a hexadecimal value of the proper length (40 hexadecimal digits):
187+
188+
```js
189+
expect("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").to.be.a.properAddress;
190+
```
191+
192+
The `properPrivateKey` matcher asserts that the given string is a hexadecimal value of the proper length:
193+
194+
```js
195+
expect("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").to
196+
.be.a.properPrivateKey;
197+
```
198+
199+
Finally, the `hexEqual` matcher accepts two hexadecimal strings and compares their numerical values, regardless of leading zeroes or upper/lower case digits:
200+
201+
```js
202+
expect("0x00012AB").to.hexEqual("0x12ab");
203+
```
204+
205+
## Dig Deeper
206+
207+
For a full listing of all of the matchers supported by this package, see [the reference documentation](/chai-matchers/reference).
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Migrating from Waffle
2+
3+
This page explains how to migrate from Waffle to Hardhat Chai Matchers, and the advantages of doing it.
4+
5+
## How to migrate
6+
7+
The `@nomicfoundation/hardhat-chai-matchers` plugin is meant to be a drop-in replacement for the `@nomiclabs/hardhat-waffle` plugin. To migrate, follow these instructions:
8+
9+
1. Uninstall the `@nomiclabs/hardhat-waffle` and `ethereum-waffle` packages:
10+
11+
::::tabsgroup{options=npm,yarn}
12+
13+
:::tab{value=npm}
14+
15+
```
16+
npm uninstall @nomiclabs/hardhat-waffle ethereum-waffle
17+
```
18+
19+
:::
20+
21+
:::tab{value=yarn}
22+
23+
```
24+
yarn remove @nomiclabs/hardhat-waffle ethereum-waffle
25+
```
26+
27+
:::
28+
29+
::::
30+
31+
2. Then install the Hardhat Chai Matchers plugin:
32+
33+
::::tabsgroup{options=npm,yarn}
34+
35+
:::tab{value=npm}
36+
37+
```
38+
npm install @nomicfoundation/hardhat-chai-matchers
39+
```
40+
41+
:::
42+
43+
:::tab{value=yarn}
44+
45+
```
46+
yarn add @nomicfoundation/hardhat-chai-matchers
47+
```
48+
49+
:::
50+
51+
::::
52+
53+
3. In your Hardhat config, import the Hardhat Chai Matchers plugin and remove the `hardhat-waffle` one:
54+
55+
::::tabsgroup{options=TypeScript,JavaScript}
56+
57+
:::tab{value=TypeScript}
58+
59+
```diff
60+
- import "@nomiclabs/hardhat-waffle";
61+
+ import "@nomicfoundation/hardhat-chai-matchers";
62+
```
63+
64+
:::
65+
66+
:::tab{value=JavaScript}
67+
68+
```diff
69+
- require("@nomiclabs/hardhat-waffle");
70+
+ require("@nomicfoundation/hardhat-chai-matchers");
71+
```
72+
73+
:::
74+
75+
::::
76+
77+
4. If you were not importing the `@nomiclabs/hardhat-ethers` plugin explicitly (because the Hardhat Waffle plugin already imported it), then add it to your config:
78+
79+
::::tabsgroup{options=TypeScript,JavaScript}
80+
81+
:::tab{value=TypeScript}
82+
83+
```ts
84+
import "@nomiclabs/hardhat-ethers";
85+
```
86+
87+
:::
88+
89+
:::tab{value=JavaScript}
90+
91+
```js
92+
require("@nomiclabs/hardhat-ethers");
93+
```
94+
95+
:::
96+
97+
::::
98+
99+
## Why migrate?
100+
101+
The Hardhat Chai Matchers are compatible with Waffle's API and offer several advantages:
102+
103+
- **More features**: the Hardhat Chai Matchers include new matchers, like [`.revertedWithCustomError`](/chai-matchers/reference#.revertedwithcustomerror) and [`.revertedWithPanic`](/chai-matchers/reference#.revertedwithpanic), which let you perform better assertions of a transaction's revert reason.
104+
- **Support for native BigInts**: Besides numbers and ethers’s BigNumbers, you can also use JavaScript's native BigInts in your assertions, which means being able to do things like `expect(await token.totalSupply()).to.equal(10n**18n)` instead of `expect(await token.totalSupply()).to.equal(ethers.BigNumber.from("1000000000000000000"))`.
105+
- **More reliable**: Several problems and minor bugs in Waffle's matchers are fixed in the Hardhat Chai Matchers.

docs/src/content/layouts.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ documentation:
88
- advanced
99
- troubleshooting
1010
- reference
11+
- chai-matchers
12+
- network-helpers
1113
- plugins
1214
- metamask-issue
1315
- errors
14-
- chai-matchers
15-
- network-helpers
1616

1717
tutorial:
1818
title: tutorial
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
section-type: hidden
1+
section-type: group
2+
section-title: Hardhat Network Helpers
3+
order:
4+
- href: /
5+
title: What is it?
6+
- /reference
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Network Helpers <!-- An Explanation: theoretical, not practica; for studying, not for working -->
2+
3+
<!-- What is it? -->
4+
<!-- How can I use it? -->
5+
<!-- Why would I want to use it? -->
6+
7+
[@nomicfoundation/hardhat-network-helpers](https://www.npmjs.com/package/@nomicfoundation/hardhat-network-helpers) provides convenience functions for working with [Hardhat Network](/hardhat-network).
8+
9+
Hardhat Network exposes its custom functionality primarily through its JSON-RPC API. See the extensive set of methods available in [its reference documentation](../hardhat-network/reference#hardhat-network-methods). However, for easy-to-read tests and short scripts, interfacing with the JSON-RPC API is too noisy, requiring a verbose syntax and extensive conversions of both input and output data.
10+
11+
This package provides convenience functions for quick and easy interaction with Hardhat Network. Facilities include the ability to mine blocks up to a certain timestamp or block number, the ability to manipulate attributes of accounts (balance, code, nonce, storage), the ability to impersonate specific accounts, and the ability to take and restore snapshots.
12+
13+
For a full listing of all of the helpers provided by this package, see [the reference documentation](/network-helpers/reference).

docs/src/global-tabs.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ type TabType = string;
44

55
interface ISelectedTabsState {
66
"npm/yarn": string;
7-
"js/ts": string;
7+
"TypeScript/JavaScript": string;
88
[key: TabType]: string;
99
}
1010

@@ -16,21 +16,21 @@ interface ITabsContext {
1616
export const GlobalTabsContext = React.createContext<ITabsContext>({
1717
tabsState: {
1818
"npm/yarn": "npm",
19-
"js/ts": "js",
19+
"TypeScript/JavaScript": "TypeScript",
2020
},
2121
changeTab: () => {},
2222
});
2323

2424
export const generateTabsGroupType = (options: string): string => {
25-
return options.split(",").sort().join("/");
25+
return options.split(",").join("/");
2626
};
2727

2828
export const TabsProvider = ({
2929
children,
3030
}: React.PropsWithChildren<{}>): JSX.Element => {
3131
const [tabsState, setTabsState] = useState<ISelectedTabsState>({
3232
"npm/yarn": "npm",
33-
"js/ts": "js",
33+
"TypeScript/JavaScript": "TypeScript",
3434
});
3535

3636
const changeTab = useCallback(

0 commit comments

Comments
 (0)