@mangrovedao/hardhat-test-solidity v0.0.17
hardhat-test-solidity
💡 Inspired by dapptools's ds-test.
⚠️ Requires hardhat-deploy as a peer dependency.
tl;dr
Tests suites are solidity contracts, tests are solidity functions.
Install
terminal
$ npm install @mangrovedao/hardhat-test-solidity
hardhat.config.js
require('@mangrovedao/hardhat-test-solidity');
MyContract_Test.sol:
// Adds Test library to the context import Test from "@mangrovedao/hardhat-test-solidity/test.sol";
// _Test
suffix means it is a test contract
contract MyContract_Test {
receive() external payable {} // necessary to receive eth from test runner
// _test
suffix means it is a test function
function addition_test() public {
prepare();
// Logging will be interpreted by hardhat-test-solidity
Test.eq(4,2+2,"oh no");
}
// Will not be interpreted as a test function function prepare() public {} }
> terminal
$ npx hardhat test-solidity MyContract
## How to structure test contract
* All contracts with suffix `_Test` are test contracts.
* All public functions with suffix `_test` are test functions.
* All public functions with suffix `_beforeAll` setup the state before other tests.
* If you have the functions `fn_test` and `fn_before`, `fn_before` will run, then `fn_test`, without state revert.
### Tips
* You must make a `receive` function so your test contracts get ethers.
* State is reverted between tests
* Functions are sorted alphabetically before they are added to test suite
* You probably want a `MyContract_Test` for every `MyContract`.
* Having multiple `_beforeAll` functions rather than setting things up in the constructor means you can split setup into multiple transactions to go around gas limits.
## How to test for stuff
* `Test.check(bool success,string memory message)` succeeds if `success` is true.
* `Test.eq(actual,expected,message)` for testing `bytes32`, `bool`, `string`, `uint`, `address` equality.
* `Test.eq0(actual,expected,message)` for testing `bytes` equality.
* `Test.less(uint a, uint b,message)` succeeds if `a < b`.
* `Test.more(uint a, uint b,message)` succeeds if `a > b`.
* `Test.fail(message)` to always fail.
* `Test.succeed()` to always succeed.
## How to test for event emission
Suppose you want to make sure that contract `Market` emits the `Trade` event.
import "@mangrovedao/hardhat-test-solidity/test.sol"; contract My_Test { receive() external payable {} // necessary to receive eth from test runner
Market amm;
function _beforeAll() { amm = new Market() }
function first_test() { Market.trade();
Test.expectFrom(amm);
emit Market.Trade();
} }
So: you _emit_ an event (with any arguments you like), and the plugin will check that `amm` has already emitted the exact same event.
### Tips
* For a given address, the order of events matters. Between addresses it does not.
* Events are checked _after their emission_. So do all your test, and at the end test for events.
* If you want to normally emit events from a test after you already called `Test.expectFrom(address)`, call `Test.stopExpecting()`;
## How to use the command line
`npx hardhat test-solidity [contract names without _Test] [--prefix function_prefix]`
Add `--details` for more detailed logging, including logs generated by the `logFormatters` plugin option (see below).
For more CLI options look at
> `npx hardhat test-solidity --help`
### Tip
If you want to only run the test `breath_is_fire_test` in the testing contract `Dragon_Test`, and you have 10 testing contracts, run
```test-solidity Dragon --prefix breath_is_fire```
rather than just
```test-solidity --prefix breath_is_fire```
so you don't waste time deploying all the other test suite contracts.
## How to log
To get nicely-formatted logs, use the `Display` library. There are
* `Display.log(uint|string)`
* `Display.log(uint|string,uint|string)`
* `Display.log(uint|string,uint|string,uint|string)`
* `Display.logBalances(address[1] memory tokens, address a0)`
* `Display.logBalances(address[1] memory tokens, address a0, address a1)`
* `Display.logBalances(address[1] memory tokens, address a0, address a1, address a2)`
* `Display.logBalances(address[2] memory tokens, address a0)`
* `Display.logBalances(address[2] memory tokens, address a0, address a1)`
* `Display.logBalances(address[2] memory tokens, address a0, address a1, address a2)`
## How to register addresses
To get pretty-printing of addresses, in your test setup do
```solidity
import {Display as D} from "@mangrovedao/hardhat-test-solidity/test.sol";
...
D.register(address addr, string memory name)
then when using --show-events
and wherever addresses are used, name
will be shown instead of addr
.
How to configure the plugin
hardhat.config.js
{ ..., testSolidity: { // default values as follows: timeout: 300_000 /* test suite timeout in ms */, logFormatters: (hre,formatArg) => { return {}; } /* format logs */ testers: (hre,formatArg,assert) => { return {}; } /* format logs */ } }
Custom log formatters
logFormatters(hre,formatArg):object
is a function that takes the hre
hardhat runtime environment and a formatArg(arg,type?):string
utility function. arg
is dynamically tested and type
is an optional type hint (it can be uint
, address
, or an array of type hints) to help formatting.
logFormatters
should return an object where keys are event names and values are formatting functions that should directly log to console and have type:
(log:ethers.LogDescription,rawLog:{topics,data},originator:string):void
Tip
See src/logFormatters.js
for examples.
Custom testers/assertions
testers(hre,formatArg,assert/*assert library*/)
takes the hre
, formatArg
, and the chai assert object, and returns an object where keys are test event names and values are of the form :
{
trigger({success,message,actual,expected}) : void
}
You should create the corresponding events in your tests so that they can be interpreted by your functions.
Tip
See src/testers.js
for examples.
Debugging
This plugin uses the debug package. To debug this plugin only do:
DEBUG='hardhat:test-solidity:*' npx hardhat test-solidity [args]