2022.2.26
Ganache,mocha
我们已经实现了合约的编译和部署的自动化,这将大大提升我们开发的效率。 但流程的自动化并不能保证我们的代码质量。质量意识是靠谱工程师的基本职业 素养,在智能合约领域也不例外:任何代码如果不做充分的测试,问题发现时通 常都已为时太晚;如果代码不做自动化测试,问题发现的成本就会越来越高。
在编写合约时,我们可以利用 remix 部署后的页面调用合约函数,进行单元 测试;还可以将合约部署到私链,用 geth 控制台或者 node 命令行进行交互测 试。但这有很大的随意性,并不能形成标准化测试流程;而且手动一步步操作, 比较繁琐,不易保证重复一致。
于是我们想到,是否可以利用现成的前端技术栈实现合约的自动化测试呢? 当然是可以的,mocha 就是这样一个 JavaScript 测试框架。
开始编写测试脚本之前,我们首先需要安装依赖:测试框架 mocha。当然,作为对合约的测试,模拟节点 ganache 和 web3 都是不可缺少的;不过我们在 上节课编写部署脚本时,已经安装了这些依赖(我们的 web3 依然是 1.0.0 版本)。
npm install mocha –save-dev进行单元测试,比较重要的一点是保证测试的独立性和隔离性,所以我们并 不需要测试网络这种有复杂交互的环境,甚至不需要本地私链保存测试历史。而 ganache 基于内存模拟以太坊节点行为,每次启动都是一个干净的空白环境,所以非常适合我们做开发时的单元测试。还记得 ganache 的前身叫什么吗?就是大名鼎鼎的 testRPC。
mocha 是 JavaScript 的一个单元测试框架,既可以在浏览器环境中运行,也可以在 node.js 环境下运行。我们只需要编写测试用例,mocha 会将测试自动运行并给出测试结果。 mocha 的主要特点有:
假设我们编写了一个 sum.js,并且输出一个简单的求和函数:
module.exports = function (rest) { var sum = 0; for (let n of rest) { sum += n; } return sum;};这个函数非常简单,就是对输入的任意参数求和并返回结果。 如果我们想对这个函数进行测试,可以写一个 test.js,然后使用 Node.js 提 供的 assert 模块进行断言:
const assert = require('assert');const sum = require('../scripts/sum');assert.strictEqual(sum(), 0);assert.strictEqual(sum(1), 1);assert.strictEqual(sum(1, 2), 3);assert.strictEqual(sum(1, 2, 3), 6);assert 模块非常简单,它断言一个表达式为 true。如果断言失败,就抛出 Error。
单独写一个 test.js 的缺点是没法自动运行测试,而且,如果第一个 assert 报错,后面的测试也执行不了了。
如果有很多测试需要运行,就必须把这些测试全部组织起来,然后统一执行, 并且得到执行结果。这就是我们为什么要用 mocha 来编写并运行测试。
我们利用 mocha 修改后的测试脚本如下:
const assert = require('assert');const sum = require('../scripts/sum');describe('#sum.js', () => { describe('#sum()', () => { it('sum() should return 0', () => { assert.strictEqual(sum(), 0); }); it('sum(1) should return 1', () => { assert.strictEqual(sum(1), 1);});it('sum(1, 2) should return 3', () => { assert.strictEqual(sum(1, 2), 3); }); it('sum(1, 2, 3) should return 6', () => { assert.strictEqual(sum(1, 2, 3), 6); }); });});里我们使用 mocha 默认的 BDD-style 的测试。describe 可以任意嵌套, 以便把相关测试看成一组测试。
describe 可以任意嵌套,以便把相关测试看成一组测试;而其中的每个 it 就代表一个测试。
每个 it("name", function() {...})就代表一个测试。例如,为了测试 sum(1, 2), 我们这样写:
it('sum(1, 2) should return 3', () => { assert.strictEqual(sum(1, 2), 3);});编写测试的原则是,一次只测一种情况,且测试代码要非常简单。我们编写多个测试来分别测试不同的输入,并使用 assert 判断输出是否是我们所期望的。
下一步,我们就可以用 mocha 运行测试了。打开命令提示符,切换到项目目录,然后创建文件夹 test,将 test.js 放入 test 文件夹下,执行命令:
./node_modules/mocha/bin/mocha test.jsmocha 就会自动执行 test 文件夹下所有测试,然后输出如下:
#sum.js #sum()✓ sum() should return 0✓ sum(1) should return 1✓ sum(1, 2) should return 3✓ sum(1, 2, 3) should return 64 passing (7ms)这说明我们编写的 4 个测试全部通过。如果没有通过,要么修改测试代码, 要么修改 hello.js,直到测试全部通过为止。
测试时我们通常会把每次测试运行的环境隔离开,以保证互不影响。对应到合约测试,我们每次测试都需要部署新的合约实例,然后针对新的实例做功能测试。 Car 合约的功能比较简单,我们只要设计 2 个测试用例:
新建测试文件 tests/car.spec.js,完整的测试代码如下。
const path = require('path');const assert = require('assert');const ganache = require('ganache-cli');const Web3 = require('web3');// 1. 配置 providerconst web3 = new Web3(ganache.provider());// 2. 拿到 abi 和 bytecodeconst contractPath = path.resolve(__dirname,'../compiled/Car.json');const { interface, bytecode } = require(contractPath);
let accounts;let contract;const initialBrand = 'BMW';
describe('contract', () => {// 3. 每次跑单测时需要部署全新的合约实例,起到隔离的作用 beforeEach(async () => {accounts = await web3.eth.getAccounts(); console.log('合约部署账户:', accounts[0]); contract = await newweb3.eth.Contract(JSON.parse(interface)).deploy({ data: bytecode, arguments: [initialBrand] }).send({ from: accounts[0], gas: '1000000' }); console.log('合约部署成功:', contract.options.address); });// 4. 编写单元测试it('deployed contract', () => { assert.ok(contract.options.address);});it('should has initial brand', async () => {const brand = await contract.methods.brand().call(); assert.equal(brand, initialBrand);}); it('can change the brand', async ()=>{ const newBrand = 'Benz'; await contract.methods.setBrand(newBrand) .send({from: accounts[0]}); const brand = await contract.methods.brand().call(); assert.equal(brand, newBrand); });});部署合约
kimshan@MacBook-Pro contract_workflow % ./node_modules/.bin/mocha tests/car.spec.js
contract合约部署账户: 0xeb35932Ab3fa7967409F6c7333b4AfE7b345E4aB合约部署成功: 0xd14C19f1DE8cfC3Ce8fD02a19DebE38bA93c107D ✔ deployed contract合约部署账户: 0xeb35932Ab3fa7967409F6c7333b4AfE7b345E4aB合约部署成功: 0x6883F1eebF9Bb7DC1b6cA3b01240aB819467Ad50 ✔ should has initial brand合约部署账户: 0xeb35932Ab3fa7967409F6c7333b4AfE7b345E4aB合约部署成功: 0x93Ad119F4d3AE3554b702CC5a34A8A0C2430E2d4 ✔ can change the brand (81ms)
3 passing (400ms)const path = require('path');const assert = require('assert');const ganache = require('ganache-cli');const Web3 = require('web3');// 1. 配置 providerconst web3 = new Web3(ganache.provider());// 2. 拿到 abi 和 bytecodeconst contractPath = path.resolve(__dirname,'../compiled/Connect.json');const { abi, evm } = require(contractPath);const interface = abi;const bytecode = evm.bytecode.object;
let accounts;let contract;const initialBrand = 'Name';const initialPrise = 0;const newBrand = 'New';const newPrise = 1;
describe('contract', () => { // 3. 每次跑单测时需要部署全新的合约实例,起到隔离的作用 beforeEach(async () => { accounts = await web3.eth.getAccounts(); console.log('合约部署账户:', accounts[0]); contract = await new web3.eth.Contract(interface) .deploy({ data: bytecode, arguments: [initialBrand,initialPrise] }) .send({ from: accounts[0], gas: '1000000' }); console.log('合约部署成功:',contract.options.address); });
// 4. 编写单元测试 it('deployed contract', () => { assert.ok(contract.options.address); });
it('should has initial brand', async () => { const brand = await contract.methods.getBrand().call(); assert.equal(brand, initialBrand); });
it('should has initial prise', async () => { const prise = await contract.methods.getPrice().call(); assert.equal(prise, initialPrise); });
it('should has new brand', async () => { await contract.methods.setBrand(newBrand).send({from: accounts[0]}); const brand = await contract.methods.getBrand().call(); assert.equal(brand, newBrand); });
it('should has new prise', async () => { await contract.methods.setPrice(newPrise).send({from: accounts[0]}); const prise = await contract.methods.getPrice().call(); assert.equal(prise, newPrise); });});部署合约
kimshan@MacBook-Pro scripts % npm run test
> contract_workflow@1.0.0 pretest /Users/kimshan/workplace/blockchain/connect> npm run compile
> contract_workflow@1.0.0 compile /Users/kimshan/workplace/blockchain/connect> node scripts/compile.js
save compiled contract Connect to /Users/kimshan/workplace/blockchain/connect/compiled/Connect.json
> contract_workflow@1.0.0 test /Users/kimshan/workplace/blockchain/connect> mocha tests/
contract合约部署账户: 0xb863B44Bb2EE9E1F7D619B318eF438e2f932dE66合约部署成功: 0x121798DFE9192756672C2cC50254eF142Fd64865 ✔ deployed contract合约部署账户: 0xb863B44Bb2EE9E1F7D619B318eF438e2f932dE66合约部署成功: 0x322F3792Ee504F628aB98ceEAcc7f9eF74282e42 ✔ should has initial brand合约部署账户: 0xb863B44Bb2EE9E1F7D619B318eF438e2f932dE66合约部署成功: 0x2BfcDADba49751C5eDe8d09a44744C41D1827D7A ✔ should has initial prise合约部署账户: 0xb863B44Bb2EE9E1F7D619B318eF438e2f932dE66合约部署成功: 0xd4BaC4e71c82d0834a0F8703FFB9Ee8332Fcb902 ✔ should has new brand (70ms)合约部署账户: 0xb863B44Bb2EE9E1F7D619B318eF438e2f932dE66合约部署成功: 0x1c034d941A47EEFB4d7CA96da27C19A8738d1E9f ✔ should has new prise (54ms)
5 passing (607ms)