使用Hardhat、Chai/Mocha测试Solidity库合约的方法及报错解决
Solidity 场景下 Hardhat + Chai/Mocha 测试库合约最佳实践
报错根因说明
你遇到的Types containing (nested) mappings can only be parameters or return variables of internal or library functions是Solidity原生语法限制:所有包含mapping(含嵌套mapping)的类型,仅允许作为internal函数、库函数的入参或返回值,无法作为public/external对外函数的参数或返回值——这类类型不具备标准ABI编码能力,无法和链下交互。
直接把库改写为普通合约测试的思路本身不成立:库中很多面向内部调用设计的、接收带mapping的storage类型的逻辑,改成普通合约后会被识别为对外函数,直接触发编译报错。
推荐测试方案
全程不需要修改原有业务库的代码,避免破坏生产逻辑:
- 编写专用测试包装合约(最通用,覆盖所有库场景)
单独创建仅用于测试的包装合约,在合约内部声明库对应的带mapping的状态变量,通过using 库名 for 类型语法引入库方法,把需要测试的库逻辑包装成不带mapping类型参数/返回值的external方法,对外只暴露需要交互的入参、需要断言的具体字段值。
示例代码:
测试时直接部署这个// 原有业务库,保持生产代码原样不做任何修改 library AssetLib { struct AssetStore { mapping(address => uint256) userHoldings; uint256 totalSupply; } function mint(AssetStore storage self, address to, uint256 amount) internal { self.userHoldings[to] += amount; self.totalSupply += amount; } } // 测试专用包装合约,单独放在test目录下 contract AssetLibTestHelper { using AssetLib for AssetLib.AssetStore; AssetLib.AssetStore internal _store; // 带mapping的状态变量存在合约内部,不对外暴露 // 包装库的mint方法,入参只传链下可编码的普通类型 function mint(address to, uint256 amount) external { _store.mint(to, amount); } // 对外暴露需要断言的具体字段,不要返回整个AssetStore结构体 function getHolding(address user) external view returns (uint256) { return _store.userHoldings[user]; } function getTotalSupply() external view returns (uint256) { return _store.totalSupply; } }AssetLibTestHelper合约,调用它暴露的方法即可,完全不会触发mapping类型的编译错误,也能100%覆盖原库的逻辑。 - 直接链接库测试(仅适用于无storage操作的纯函数库)
如果你的库全是不涉及storage操作的纯计算逻辑,没有任何带mapping的类型传参,可以直接在Hardhat脚本中部署库合约,再把库地址链接到测试调用合约上,直接调用库的public方法即可。 - Chai断言注意事项
不要尝试在JS/TS测试代码中接收、断言带mapping的结构体,只需要通过包装合约暴露的视图方法拿到具体的基础类型值,再用Chai的equal、deepEqual等方法做断言即可。如果要测事件触发,直接在包装合约里透传原库触发的事件,在测试脚本里监听即可。
避坑要点
- 不要为了适配测试修改原库的函数可见性,比如把internal方法改成public,既会破坏库的调用语义,还会给生产合约留下不必要的公开接口风险。
- 不要尝试在链下测试脚本中直接传入带mapping的类型参数,EVM没有针对这类类型的ABI编码标准,调用必然失败。
- 不需要额外配置即可让Hardhat的
solidity-coverage插件识别包装合约的调用,正确统计原库的测试覆盖率。
内容的提问来源于stack exchange,提问作者TheScaryTriangle
相关产品推荐
相关产品推荐

