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.js
mocha 就会自动执行 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 6
4 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. 配置 provider
const web3 = new Web3(ganache.provider());
// 2. 拿到 abi 和 bytecode
const 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 new
web3.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. 配置 provider
const web3 = new Web3(ganache.provider());
// 2. 拿到 abi 和 bytecode
const 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)