与其他软件程序不同,智能联系人一旦部署到特定地址中就无法修改或删除。这种独特的限制使智能联系人中的漏洞比其他漏洞更加危险。因此,需要更详尽的测试。
当前用于Solidity单元测试的最著名的单元测试工具如下
- 松露智能合约测试框架
- Remix-IDE单元测试插件
- OpenZeppelin测试环境
OpenZeppelin测试环境最近出现了,尽管看起来很有希望,但似乎还没有得到足够的验证。Remix是最强大的编辑工具之一。但是在命令行模式下比在GUI模式下单元测试更合适。因此,到目前为止,最推荐使用Truffle智能合约测试框架。
下面的图像链接是针对ERC-20智能合约的松露单元测试程序。
在测试程序中,使用了Truffle文档中未包含的一些策略。它们可以使测试程序更加有效和高效,下面将对其进行说明。
如果您不熟悉Truffle测试框架,最好先阅读官方文档[1]。 福州小程序开发公司
[1]松露-用JavaScript编写测试
分组测试案例
松露测试框架采用了著名的Mocha [1]。Truffle测试程序稍微扭曲了Mocha测试的基本结构,从contract()
功能开始,并在其中包含it()
测试用例的功能[2]。
合同(“ ERC20常规合同测试套件”,异步帐户=> { 它(“应该在构造函数中指定'名称'和'符号'...”,async()=> { ... }); 它(“应该有0个补给,不要暂停,并且在开始时为所有帐户设置0个余额。”,async()=> { ... }); 它(“铸造令牌可以增加所有者的平衡和总供应量吗,” async()=> { ... }); 它(“只能由铸造者铸造令牌(授予铸造者角色的帐户)。”,async()=> { ... }); ... 它(“可以转移减少发件人余额并增加接收人余额的数量。”,async()=> { ... }); 它(“不应更改无关帐户的余额(无论是发件人还是收件人)。”,async()=> { ... }); ... });
如果智能合约具有数十个单元测试,则上述的线性结构将变得难以阅读,维护和更新。Mocha允许将测试用例嵌套到中间describe()
函数中,因此Truffle测试用例可以使用嵌套describe()
函数对测试用例进行分组以提高可读性。
在下面的测试程序,测试例被分组为Initial State
,Minting
,Transfer
,Approval
,Delegated Transfer
,Burning
,和Circuit Breaker
根据令牌顶层概念。这些组是describe()
函数,并且it()
在下面包含作为测试用例的函数。
合同(“ ERC20Regular Contract Test Suite”,异步帐户=> { 描述(“初始状态”,()=> { it(“应在构造函数中指定'name'和'symbol',并...”,async()=> { ... }); 它(“应有零供应,不能暂停,并为所有...设置零余额。”,async()=> { ... }); }); describe(“ Minting”,()=> { it(“铸币可以使所有者的余额和总供应量增加很多吗,” async()=> { ... }); 它(“只能由铸造者铸造令牌(授予铸造者角色的帐户)。”,async()=> { ... }); 它(“铸造后应触发'Transfer'事件。”,async()=> { ... }); }); describe(“ Transfer”,()=> { 它(“可以转移减少发件人的余额并增加接收人的余额。”,async()=> { ... }); ... it(“不得更改无关帐户的余额(无论是发送者还是接收者)。”,async()=> { ... }); 它(“转移后根本不应该改变总供给。”,async()=> { ... }); ... }); 描述(“批准”,()=> { ... }); describe(“委派转移”,()=> { ... }); describe(“ Burning”,()=> { ... }); describe(“ Circuit Breaker”,()=> { ... }); });
对于许多测试用例,如果每次都执行所有现有的测试用例,则测试新添加的测试用例的效率可能非常低。将测试用例分成几个测试程序来避免这种情况会引起其他不一致性和可维护性方面的问题。您可以使用only()
函数仅运行要使用Mocha framework [3]测试的选定测试用例。
如果执行下面的测试程序,将仅运行Transfer
类别下describe()
带有功能标记为的测试用例only()
。
合同(“ ERC20常规合同测试套件”,异步帐户=> { describe(“ Initial State”,()=> { ... }); describe(“ Minting”,()=> { ... }); describe.only(“ Transfer”,()=> { ... }); describe(“ Approval”,()=> { ... }); describe(“委托转帐”,()=> { ... }); ... });
您可以应用only()
到it()
的功能,以缩小执行范围等等。可以在测试程序中标记多个describe()
或it()
功能only()
。
执行下面的测试程序,仅以2个开头的测试用例it.only
将跳过所有其他测试用例而运行。
合同(“ ERC20常规合同测试套件”,异步帐户=> { describe(“ Initial State”,()=> { ... }); describe(“ Minting”,()=> { ... }); describe(“ Transfer”,()=> { 它(“可以转移减少发件人的余额并增加接收人的余额。”,async()=> { ... }); ... it.only(“不得更改无关帐户的余额(无论是发件人还是收件人)。”,async()=> { ... }); it.only(“转移后完全不应更改总供给。”,async()=> { ... }); ... }); describe(“ Approval”,()=> { ... }); ... });
[1] Mocha :功能丰富的JavaScript测试框架
[2] JavaScript中的松露测试
[3] Mocha / Exclusive Tests
大数字
以太坊更喜欢大量数字。以太坊中的隐式单位是wei,最具代表性的以太是10 18 wei [1]。
在JavaScript中,原始数字类型的最大整数值约为2 53(〜10 16)[2]。因此,要使用JavaScript处理以太坊,必须使用另一种用于大量数字的数字类型。web3.js
[3]是以太坊最基本的小工具之一bn.js
[4]和bignumber.js
[5]。value
参数的数据类型和功能中的gasPrice
参数web3.eth.sendTransaction()
显示此信息。尽管不清楚为什么web3.js
支持两种不同类型的大数,但考虑到web3.utils.BN()
和web3.utils.toBN()
,似乎BN
(bn.js
)是首选。
为了在测试程序中更轻松地处理大数web3.utils.toBN()
,请在开始时定义对函数的引用。
const toBN = web3.utils.toBN;
BN
(bn.js
)API包含各种功能,包括算术运算,比较运算和按位运算。某些函数用后缀命名,n
这意味着操作数应为原始数字类型。
在下面的示例代码,与正常的名字,如功能add()
,div()
,sub()
,eq()
取BN
类型的操作数和功能与后缀命名n
如addn()
,divn()
,muln()
,eqn()
具有原始号码类型的操作数。
const toBN = web3.utils.toBN; toBN(1E19)。加(toBN(1E19)); //添加BN类型的操作数 toBN(1E19)。addn(1E5); //添加原始数字类型的操作数 toBN(1E19)。div(toBN(1E16)); //除以BN类型的操作数 toBN(1E19)。divn(1E6); //除以原始数字类型的操作数 toBN(2E19)。子(toBN(1E19))。eq(toBN(1E19)); //等于BN类型的操作数 toBN(2E19)。mul(0)。eqn(0); //等于原始数字类型的操作数
[1]以太
[2] Number.MAX_SAFE_INTEGER
[3] web3.js
:以太坊JavaScript API
[4] bn.js
:纯javascript中的BigNum
[5] bignumber.js
:用于任意精度算术的JavaScript库
随机测试数据
使用随机测试数据[1] [2] [3]是增加测试覆盖率并避免意外测试结果的最简单方法之一。
下面是测试用例,以检查在构造函数中指定的名称和符号是否正确设置并查询令牌合同。如果令牌合同将名称字段设置为“ RGB”的硬编码值,尽管它肯定是有缺陷的,但以下测试用例找不到它。测试用例意外地使用与测试数据相同的文字“ RGB”,测试不会失败。
it(“应在构造函数中指定'name'和'symbol',并...”,async()=> { const name ='颜色标记' ; const symbol ='RGB' ; const admin = accounts [0] ; const token = await Token.new(name,symbol,{from:admin}); assert.equal(await token.name(),名称) ; assert.equal(await token.symbol(),symbol) ; assert.isTrue((等待令牌.decimals())。eqn(18)); });
为避免此类意外结果,我们可以使用以下示例中的随机测试数据。机会[4]是一个JavaScript库,用于生成各种格式和约束的随机数据。Chance的一些功能用于产生随机的句子和单词或从数组中选择一个元素。
chance.sentence({words: 3}) |
生成由3个单词组成的随机句子 |
chance.word({length: chance.natural({min: 1, max: 5})}) |
生成长度为1〜5的随机单词 |
chance.pickone(accounts) |
accounts 以一种无法预测的方式从数组中选择一个元素 |
it(“应在构造函数中指定'name'和'symbol',并...”,async()=> { 常量机会=新的Chance(); const名称=机会。句子({words:3}) ; const symbol = chance.word({length:机会.natural({min:1,max:5})}). toUpperCase(); const admin = chance.pickone(accounts) ; const token = await Token.new(name,symbol,{from:admin}); console.debug(`已部署新令牌协议-名称:$ {name},符号:$ {symbol},地址:$ {token.address}`); //查询并验证令牌的名称,符号和小数 assert.equal(等待token.name(),名称); assert.equal(await token.symbol(),symbol); assert.isTrue((等待令牌.decimals())。eqn(18)); });
在接下来的样品,随机生成用于设定量薄荷(balance
),量转移(delta
),并且占发件人和收件人(sender
,recipient
)。在要转移的金额的情况下,chance.bool({likelihood: 10})
用于边界条件(零金额)的尝试要占10%左右。
它(“可以转移减少发件人的余额并增加接收人的余额。”,async()=> { 常量机会=新的Chance(); const admin = chance.pickone(accounts) ; const token = await Token.new('Color Token','RGB',{from:admin}); console.debug(`已部署新令牌协议-地址:$ {token.address}`); //为所有帐户创建初始余额 令余额= 0; 的(帐户的常量帐户){ 余额= toBN(1E19).muln(偶然。自然({min:1,max:100})); 等待token.mint(acct,balance,{from:admin}); } ... for(让i = 0; i <循环; i ++){ [发件人,收件人] =机会。pickset(帐户,2); senderBal1 =等待token.balanceOf(sender); receiveBal1 =等待token.balanceOf(收件人); delta =机会.bool({likelihood:10})吗? toBN(0):toBN(1E10).muln(机会。自然({min:1,max:1000000})); 等待token.transfer(收件人,增量,{来自:发送者}); senderBal2 =等待token.balanceOf(sender); receiveBal2 =等待token.balanceOf(收件人); assert.isTrue(senderBal2.eq(senderBal1.sub(delta))); assert.isTrue(recipientBal2.eq(recipientBal1.add(delta))); } });
机会为各种类型或格式提供了80多种功能,包括数字,文本,日期时间,位置等。在每个功能中,可以通过选项设置详细的方面或约束。
机会。布尔({likelihood:30}); // 30%概率为“ true” 机会。字符({alpha:true,数字:true,符号:false,大小写:'lower'}); //小写字母和数字中的单个字符 机会。整数({min:-273,max:10000}); // -273和10,000之间的整数 机会。自然({max:2048}); // 0到2,048之间的自然数 机会。素数({min:1E5,max:1E5}); //介于100,000和1,000,000之间的质数 机会。字({length:5});//一个长度为5的单词 机会。句子({word:7});//一个7字的句子 机会。颜色({格式:“十六进制”,大写字母:“上部”});// RGB颜色代码,例如“#2F3AE7” 机会。电子邮件(); //电子邮件地址 机会。ip(); //一个IPv4地址 机会。国家();// ISO 3166-1 alpha-2(美国KR)中的2个字母的国家/地区代码 机会。语言环境(); // ISO 639-1中的2个字母的语言环境代码(ko,en,es,pt) 机会。日期({year:2020});// 2020年中的Date对象 机会。时间戳记(); //任何UNIX Epoch时间 机会。guid(); //一个UUID(GUID)-https://en.wikipedia.org/wiki/UUID 机会。pickone([ 'MON', 'TUE', 'WED', 'TUR', 'FRI']); //给定数组中的任何元素 机会。镐([1,2,3,4,5],3); //给定数组中的3个不同元素
[1]随机测试数据(MSDN)
[2]随机测试(Wikipedia)
[3]单元测试指南
[4]机会:随机字符串,数字等的极简生成器。
还原事件
智能合约与外部系统异步交互,因此不能通过返回值来传递交易结果。而是触发事件并发出交易收据。为了更彻底地确认智能合约的行为是否符合预期,智能合约测试应验证触发的事件。智能合约测试还应该在失败情况下检查设计或计划的还原,例如无效的输入值,特权不足,余额不足等。
要检查还原或事件,在发送交易后需要处理交易收据[1] [2]。该代码可能有点冗长,因此如果有便利功能,它将很有用。令我惊讶的是,Truffle测试框架没有提供任何服务。但是我们可以使用以下库。
- 松露断言
- OpenZeppelin测试助手
当前,这两个库提供相似的功能,由于OpenZeppelin的名称值,因此首选后者。
要使用OpenZeppelin测试助手[3] [4],必须导入@openzeppelin/test-helpers
模块。
const令牌= artifacts.require(“ ERC20Regular”); const Chance = require('chance'); const toBN = web3.utils.toBN; const {常量,ExpectEvent,ExpectRevert} = require('@ openzeppelin / test-helpers') ;
要使用失败测试用例确认事务已还原,请使用expectRevert.unspecified()
函数。
它(“只能由铸造者铸造令牌(授予铸造者角色的帐户)。”,async()=> { 常量机会=新的Chance(); const admin = chance.pickone(accounts); const token = await Token.new('Color Token','RGB',{from:admin}); console.debug(`已部署新令牌协议-地址:$ {token.address}`); 让tryer = null; //选择除admin以外的任何帐户 做{tryer =机会。皮克(帐户); } while(tryer == admin) 令amt = 0; 的(帐户的常量帐户){ amt = toBN(1E17).muln(chance.natural({min:1,max:100})); 等待ExpectRevert.unspecified(token.mint(acct,amt,{from:tryer}))); } });
它(“无法从任何帐户转移到零地址”,async()=> { 常量机会=新的Chance(); const admin = chance.pickone(accounts); const token = await Token.new('Color Token','RGB',{from:admin}); console.debug(`已部署新令牌协议-地址:$ {token.address}`); 令余额= 0; 的(帐户的常量帐户){ 余额= toBN(1E9).muln(chance.natural({min:1,max:100})); 等待token.mint(acct,balance,{from:admin}); } 令delta = 0; 的(帐户的常量帐户){ delta = toBN(1E3).muln(chance.natural({min:0,max:100})); 等待ExpectRevert.unspecified(token.transfer(constants.ZERO_ADDRESS,delta,{from:acct}))); } });
要使用通过测试用例验证事件,请使用expectEvent()
函数。可以确认事件参数,包括参数名称和值。
it(“批准后应触发“批准”事件。”,async()=> { 常量机会=新的Chance(); const admin = chance.pickone(accounts); const token = await Token.new('Color Token','RGB',{from:admin}); console.debug(`已部署新令牌协议-地址:$ {token.address}`); const循环= 10; 让所有者= 0,支出者= 0,津贴= 0; for(让i = 0; i <循环; i ++){ 所有者=机会。皮克(帐户); 消费=机会。皮克(帐户); 津贴=机会。bool({likelihood:10})吗? toBN(0):toBN(1E5).muln(chance.natural({min:1,max:1000000})); ExpectEvent(await token.approve(spender,allowance,{from:owner}), '批准',{owner:所有者,支出者:支出者,值:allowance.toString()}); } });
可以指定索引而不是事件参数名称。
ExpectEvent(await token.approve(spender,allowance,{from:owner}), '批准',{0:所有者,1:支出者,2:allowance.toString()});
[1] web3.eth.getTransactionReceipt
[2]深入了解以太坊日志
[3] OpenZeppelin Test Helpers源项目
[4] OpenZeppelin Test Helpers API参考
ECMAScript 8(2017年)
自JavaScript诞生以来已经有一段时间了,它仍在迅速发展。JavaScript由ECMAScript进行了标准化,自2015年以来[1] [2]每年都宣布新的规范版本。
为了更有效地处理Truffle测试程序,重要的是选择包含测试功能的JavaScript的正确版本。
JavaScript本质上是异步的。大多数的框架和库,包括web3.js
和松露合同抽象异步运行。使用异步进程处理回调或Promise进行编程流可能会提高性能,但可能会更加复杂和困难。对于测试程序,可读性和易用性比优化或性能更为重要。
async
[3] / await
[4]语句启用异步功能的同步流,因此代码可以避免回调堆栈。
await
语句仅在async
块中有效。在松露测试的情况下,测试函数(函数的第二个参数)it()
将成为async
函数,然后await
在测试函数内部进行调用。
下面的示例显示了所有调用令牌合同(Token.new
,token.mint
,token.totalSupply
,token.transfer
)是await
一个内部async
函数(async() => {}
)。
it(“ ...”,async()=> { 常量机会=新的Chance(); const admin = chance.pickone(accounts); const token = await Token.new('Color Token','RGB',{from:admin}); 令余额= 0; 的(帐户的常量帐户){ 余额= toBN(1E19).muln(chance.natural({min:1,max:100})); 等待token.mint(acct,balance,{from:admin}); } const total =等待token.totalSupply(); const循环= 20; 让发送者= 0,接收者= 0,增量= 0; for(让i = 0; i <循环; i ++){ 发件人=机会。皮克(帐户); 收件人=机会。皮克(帐户); delta = toBN(1E13).muln(chance.natural({min:0,max:100})); 等待token.transfer(收件人,增量,{发件人:发件人}); assert.isTrue((等待token.totalSupply())。eq(总计)); } });
在JavaScript中,使用var
关键字的变量声明具有不寻常的语义,例如函数作用域和提升[5]。这在其他编程语言中并不常见,即使使用简单的代码也会使非本机JavaScript程序员感到沮丧。2015年发布的ECMAScript 6引入了const
[6]和let
[7]语句来补偿的意外影响var
。变量声明使用const
和let
具有块作用域并且没有提升[8]。幕后的起吊实际上可能更复杂[9]。但是const
与let
相比,实际上没有吊装的地方var
。
因此,强烈建议使用const
和let
,如果打算进行功能范围界定或提升。
为了使变量声明和用法更不易出错,建议使用严格模式[10]。'use strict'
在contract()
函数的开头添加文字就足够了。这行简单的代码将删除许多容易出错的旧功能,并使您感觉更舒适。
合同(“ ERC20常规合同测试套件”,异步帐户=> { “使用严格”; if(accounts.length> 8){//避免帐户过多 accounts =(new Chance())。pickset(accounts,8); } ... )};
async
await
在ECMAScript 8和const
/let
和ECMAScript 6中添加了/语句。因此,建议使用几乎完全支持ECMAScript 8的Node.js 9.11.2或更高版本[11]。
[1] ECMAScript版本
[2] JavaScript版本
[3]async
语句
[4]await
语句
[5] JavaScript范围和吊装
[6]const
语句
[7]let
语句
[8] var和let之间的差异
[9]现代JavaScript中的吊装— let ,const和var
[10]严格模式
[11] Node.js ECMAScript兼容性表
Ganache CLI
单元测试可能非常繁琐,因此必须具有快速执行的测试环境。特别是在以太坊中,除了PoW共识算法之外,其他环境都尽可能与主网相同。智能合约单元测试基本上独立于共识算法。
因此,作为测试环境,首选具有PoA的测试网(如Rinkeby或Kovan)或具有本地独立的以太坊客户端(节点)的实现(如Ganache或Ganache CLI)。
Ganache CLI的开发时间很长,运行速度很快,并提供了各种可配置选项[1],即使在测试环境中,它也很有用。
以下命令行将启动适用于智能合约单元测试的Ganache CLI实例。
ganache-cli --networkId 31 \ --host'127.0.0.1'-端口8545 \ --gas价格2.5E10 --gasLimit 4E8 \ -确定性\- 默认余额以太10000- 帐户10-安全\ -解锁0-解锁1-解锁2-解锁3-解锁4 \ --hardfork'彼得斯堡'\ --blockTime 0 \ --db'/ var / lib / ganache-cli / data'>> /var/log/ganache.log 2>&1