|
| 1 | +# Writing unit tests in Solidity |
| 2 | + |
| 3 | +Hardhat supports writing tests in both TypeScript and Solidity. TypeScript is typically used for higher-level integration tests, whereas Solidity is better suited for unit tests. This guide explains how to add Solidity tests to a Hardhat project, run them, and configure how they are executed. It assumes familiarity with Solidity tests and isn’t meant to serve as an introduction to them. |
| 4 | + |
| 5 | +## Writing Solidity tests |
| 6 | + |
| 7 | +A Solidity file is considered a test file if it’s inside the `test/` directory, or if it’s inside the `contracts/` directory and ends with `.t.sol`. |
| 8 | + |
| 9 | +Every contract in a test file with functions that start with `test` is treated as a test contract. When the tests are run, Hardhat deploys every test contract and calls each of its test functions. |
| 10 | + |
| 11 | +For example, suppose you have a `contracts/CounterTest.t.sol` or `test/CounterTest.sol` file with the following contract: |
| 12 | + |
| 13 | +```solidity |
| 14 | +contract CounterTest { |
| 15 | + function testInc() public { |
| 16 | + Counter counter = new Counter(); |
| 17 | + counter.inc(); |
| 18 | + require(counter.count() == 1, "count should be 1"); |
| 19 | + } |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +In this case, the test runner deploys the `CounterTest` contract and calls its `testInc` function. If the function execution reverts, the test is considered failed. |
| 24 | + |
| 25 | +Hardhat also supports fuzz tests, which are similar to regular tests but accept parameters. When the tests are executed, fuzz test functions are called multiple times with random values as arguments: |
| 26 | + |
| 27 | +```solidity |
| 28 | +contract CounterTest { |
| 29 | + function testIncBy(uint by) public { |
| 30 | + Counter counter = new Counter(); |
| 31 | + counter.incBy(by); |
| 32 | + require(counter.count() == by, "count should match the 'by' value"); |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +### Assertion libraries |
| 38 | + |
| 39 | +In the previous example, the error message doesn’t show the actual value of `by` that made the test fail. That’s because interpolating the value into the string isn’t straightforward in Solidity. To get better error messages, plus other useful functionality, you can use an assertion library like [forge-std](https://github.com/foundry-rs/forge-std). |
| 40 | + |
| 41 | +To use `forge-std` in a Hardhat project, first install it: |
| 42 | + |
| 43 | +::::tabsgroup{options=npm,pnpm} |
| 44 | + |
| 45 | +:::tab{value=npm} |
| 46 | + |
| 47 | +```bash |
| 48 | +npm install --save-dev github:foundry-rs/forge-std#v1.9.7 |
| 49 | +``` |
| 50 | + |
| 51 | +::: |
| 52 | + |
| 53 | +:::tab{value=pnpm} |
| 54 | + |
| 55 | +```bash |
| 56 | +pnpm install --save-dev github:foundry-rs/forge-std#v1.9.7 |
| 57 | +``` |
| 58 | + |
| 59 | +::: |
| 60 | + |
| 61 | +:::: |
| 62 | + |
| 63 | +You can then import the `Test` base contract and extend your test contract from it. This lets you use helper functions like `assertEq`, which shows the mismatched values when the assertion fails: |
| 64 | + |
| 65 | +```solidity |
| 66 | +import { Test } from "forge-std/src/Test.sol"; |
| 67 | +
|
| 68 | +contract CounterTest is Test { |
| 69 | + function testIncBy(uint by) public { |
| 70 | + Counter counter = new Counter(); |
| 71 | + counter.incBy(by); |
| 72 | + assertEq(counter.count(), by, "count should match the 'by' value"); |
| 73 | + } |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +### Setup functions |
| 78 | + |
| 79 | +Both the unit and fuzz test examples shown above create an instance of the `Counter` contract. You can share setup logic across tests using the `setup` function, which is called before each test execution: |
| 80 | + |
| 81 | +```solidity |
| 82 | +contract CounterTest { |
| 83 | + Counter counter; |
| 84 | +
|
| 85 | + function setup() public { |
| 86 | + counter = new Counter(); |
| 87 | + } |
| 88 | +
|
| 89 | + function testInc() public { |
| 90 | + counter.inc(); |
| 91 | + require(counter.count() == 1, "count should be 1"); |
| 92 | + } |
| 93 | +
|
| 94 | + function testIncBy(uint by) public { |
| 95 | + counter.incBy(by); |
| 96 | + require(counter.count() == by, "count should match the 'by' value"); |
| 97 | + } |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +## Running Solidity tests |
| 102 | + |
| 103 | +To run all your Solidity tests, use the `test solidity` task: |
| 104 | + |
| 105 | +::::tabsgroup{options=npm,pnpm} |
| 106 | + |
| 107 | +:::tab{value=npm} |
| 108 | + |
| 109 | +```bash |
| 110 | +npx hardhat test solidity |
| 111 | +``` |
| 112 | + |
| 113 | +::: |
| 114 | + |
| 115 | +:::tab{value=pnpm} |
| 116 | + |
| 117 | +```bash |
| 118 | +pnpm hardhat test solidity |
| 119 | +``` |
| 120 | + |
| 121 | +::: |
| 122 | + |
| 123 | +:::: |
| 124 | + |
| 125 | +You can also pass one or more paths as arguments, in which case only those files are executed: |
| 126 | + |
| 127 | +::::tabsgroup{options=npm,pnpm} |
| 128 | + |
| 129 | +:::tab{value=npm} |
| 130 | + |
| 131 | +```bash |
| 132 | +npx hardhat test solidity <test-file-1> <test-file-2> ... |
| 133 | +``` |
| 134 | + |
| 135 | +::: |
| 136 | + |
| 137 | +:::tab{value=pnpm} |
| 138 | + |
| 139 | +```bash |
| 140 | +npx hardhat test solidity <test-file-1> <test-file-2> ... |
| 141 | +``` |
| 142 | + |
| 143 | +::: |
| 144 | + |
| 145 | +:::: |
| 146 | + |
| 147 | +## Configuring Solidity tests |
| 148 | + |
| 149 | +You can configure how Solidity tests are executed in your Hardhat configuration. |
| 150 | + |
| 151 | +### Configuring the tests location |
| 152 | + |
| 153 | +By default, Hardhat treats every Solidity file in the `test/` directory as a test file. To use a different location, set the `paths.tests.solidity` field: |
| 154 | + |
| 155 | +```typescript |
| 156 | +paths: { |
| 157 | + tests: { |
| 158 | + solidity: "./solidity-tests" |
| 159 | + } |
| 160 | +}, |
| 161 | +``` |
| 162 | + |
| 163 | +### Configuring the tests execution |
| 164 | + |
| 165 | +To configure how Solidity tests are executed, use the `solidityTest` property in the Hardhat configuration. |
| 166 | + |
| 167 | +For example, the `ffi` cheatcode is disabled by default for security reasons, but you can enable it: |
| 168 | + |
| 169 | +```typescript |
| 170 | +solidityTest: { |
| 171 | + ffi: true |
| 172 | +}, |
| 173 | +``` |
| 174 | + |
| 175 | +It’s also possible to modify the execution environment of the tests. For example, you can modify the address that is returned by `msg.sender`: |
| 176 | + |
| 177 | +```typescript |
| 178 | +solidityTest: { |
| 179 | + sender: "0x1234567890123456789012345678901234567890" |
| 180 | +}, |
| 181 | +``` |
0 commit comments