https://mochajs.org/ 学习网址: https://www.jianshu.com/p/9c78548caffa https://www.jb51.net/article/106463.htm 在truffle框架的简单使用中,我们了解到它的测试模块是包装了mocha测试框架的,在这里我们选择cryptopunks的truffle例子来相应讲解:
https://github.com/larvalabs/cryptopunks
为什么要使用mocha这个测试模块:
当我们在开发,我们往往会有以下的问题:
参考:https://blog.csdn.net/imwebteam/article/details/53310958
需求和开发脱节
当一份需求来了, 开发人员往往不能百分百的理解需求的内容(抛弃产品自己变更需求的可能性。。),这往往会让开发人员开发出的功能会有跟需求有所差别,这会带来额外的工作量
开发和测试脱节
什么是开发和测试脱节,说的是,当开发人员按照自己的想法开发完了一个需求。然后测试人员也按照自己的想法去测试这个需求,然后由于双方的分歧,导致测试认为开发有bug,开发认为测试是sb.
那么如何解决上面的问题呢?
答案就是 选择一种软件敏捷开发模式
敏捷开发模式
目前比较流行的开发模式有两种: TDD 和 BDD
TDD (Test Driven Development 测试驱动开发)
- 测试来驱动开发
- 其重点偏向开发
- 测试用例是在约束开发者,使开发者的目标明确,设计出满足需求的系统
BDD (Behaviour Driven Development 行为驱动开发)
- 基于TDD发展,保持测试先行的理念
- 其重点偏向设计
- 在测试代码中用一种自然通用语言的方式把系统的行为描述出来
- 将系统的设计和测试用例结合起来,进而驱动开发工作
两种方式各有其特点,我们通常选择的是BDD的方式
为了方便我们编写测试用例,我们需要使用一些前端测试用例工具——mocha
在这个例子中我们能够看见在其的test文件夹中写了一些测试文件,比如我们以cryptopunksmarket-setinitia.jsl这个文件为例,看cryptopunks测试代码cryptopunksmarket-setinitial.js
(1)Truffle测试框架学习:
参考:http://www.blockchainbrother.com/article/2082
Truffle 使用 Mocha 测试框架 和 Chai 断言来给你提供一个可靠的框架编写JavaScript测试。这里需要注意到的一个很大的不同是它使用了contract()测试套件替代了describe()测试套件,这个函数基本和 describe() 一样,只不过它可以启用clean-room 功能. 其过程如下:
(2)后面想要使用测试框架mocha,但是又不想使用truffle,所以就来学习这个框架怎么单独使用了,下面就是学习的过程:
当然,首先要安装:
使用npm全局安装:
npm install --g mocha
或者仅仅只是安装在某个模块:
npm install --save mocha
然后你就可以使用了
1.要测试上面的代码是否对的,因此就要编写测试脚本,测试脚本与所要测试的源码脚本同名,但是后缀名为 .test.js或 .spec.js, 如:xx.test.js 或 xx.spec.js
2.测试脚本可以包含一个或多个describe块,describe块称为 "测试套件",表示一组相关的测试,它是一个函数,有两个参数,第一个参数是测试套件的名称,第二个参数是一个实际执行的函数。
每个describe块也可以包含一个或多个it块,it块称为 “测试用例",表示一个单独的测试,是测试的最小单位,它也是一个函数,第一个参数也是测试用例的名称,第二个参数是一个实际执行的函数,it块之间是同步运行的。
describe 和 it 大量嵌套后,就形成了一颗树。树的非叶子节点都是测试集合,叶子节点即 it ,就是测试用例。
注意,如果一个describe 里面没有 it (比如下面:), Mocha将不会执行这个 describe。
3.理解断言库
学习文档:http://www.chaijs.com
断言库可以理解为比较函数,也就是断言函数是否和预期一致,如果一致则表示测试通过,如果不一致表示测试失败。mocha本身是不包括断言库的,所以必须引入第三方断言库的,目前比较受欢迎的断言库有 should.js, expect.js, chai.
should.js是BDD风格
expect.js是expect风格的断言
//下面介绍的是chai
chai的expect(), assert() 和 should的断言
Mocha默认使用的是BDD的风格。expect和should都是BDD的风格,二者使用相同的链式语言来组织断言的,但不同在于他们初始化断言的方式,expect使用
构造函数来创建断言对象实例,而should通过为 Object.prototype新增方法来实现断言(should不支持IE),expect直接指向 chai.expect,
should则是 chai.should();
上面的代码中 expect 是断言的意思,该作用是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误
三种的使用方法简单如下所示:
var expect = require('chai').expect;
expect(2).to.be.equal(2);
var assert = require('chai').
assert;
assert.equal((await contract.totalSupply()).toNumber(), 10);
var should = require('chai').should();
cookies.should.not.be.empty;
cookies.id.should.equal('10001','not equal to 10001');
因此在执行上面代码之前,
我们需要在项目中安装 chai, 如下命令:
npm install --save-dev chai
1)var expect = require('chai').expect;
即引用 chai 断言库,使用的是 expect断言风格。
expect 官网API(http://chaijs.com/api/bdd/).
2)mocha测试代码如何运行?
上面的add.test.js 编写完成后,我们需要运行测试代码了,进入add.test.js代码的目录后,执行如下命令可运行:
mocha add.test.js
mocha命令后面也可以指定多个文件,如下命令:
mocha xx.test.js yy.test.js
使用通配符:
或者我们可以运行如下命令,执行多个测试脚本文件:
mocha spec/{add,reduce}.js //目录spec下的add.js和reduce.js文件
mocha spec/*.js //所有文件
mocha默认运行test子目录里面的测试脚本,我们一般情况下,可以把测试脚本放在test目录下,然后进入test的上层目录,直接执行mocha命令即可:
mocha
然后你就会发现test目录下第一层的所以测试文件都被运行了。命令只会执行test第一层目录下所有文件,并不能执行嵌套目录下的文件。
为了执行所有嵌套目录下的文件,我们可以 mocha命令后面加一个参数 --recursive 参数
更多的细节信息可以看:https://www.cnblogs.com/tugenhua0707/p/8419534.html,当然之后你要补充一下
3)测试用例的钩子
Mocha在describe块之中,提供了测试用例的四个钩子,before(), after(), beforeEach()和afterEach(),他们会在指定的时间内执行。
before(): 将会在所有测试用例执行之前运行,比如在之前插入数据等等操作。
after(): 会在所有测试执行之后运行,用于清理测试环境,回滚到清空数据状态。
beforeEach(): 将会在每个测试用例执行之前执行,可用于测试测试需要准备相关数据的条件。
afterEach(): 将会在每个测试用例之后执行,可用于准备测试用例所需的后置条件。
4)理解异步钩子函数
例子1:
var expect = require('chai').expect;
describe('异步钩子函数', function() {
var foo = false;
beforeEach(function(){
setTimeout(function(){
foo = true;
}, 50)
});
it('异步钩子函数成功', function() {
expect(foo).to.be.equal(true);
})
});
结果:
异步钩子函数
异步钩子函数成功:
AssertionError: expected false to equal true
+ expected - actual
-false
+true
如上可以看到测试失败,原因是因为setTimeout 是异步的,在setTimeout执行完之前,it函数已经被执行了,所以foo当时数据还是false,
因此false不等于true了。
这时候 done参数出来了,在回调函数存在时候,它会告诉mocha,你正在编写一个异步测试,会等到异步测试完成的时候来调用done函数,或者超过2秒后超时,如下代码就可以成功了;
var expect = require('chai').expect;
describe('异步钩子函数', function() {
var foo = false;
beforeEach(function(done){
setTimeout(function(){
foo = true;
// complete the async beforeEach
done();
}, 50)
});
it('异步钩子函数成功', function() {
expect(foo).to.be.equal(true);
});
});
(3)这篇文章不是要很详细地告诉大家概念性的内容,只是让有跟我一样想法(即学习了truffle框架后,发现里面的那个测试十分有意思,想之后做测试的时候也用这种测试方法),但是突然不知道怎么入手的人一个方向,知道那是个什么测试框架,以及去哪里学习,以及一些比较简单的必须知道的概念和内容,知道这些其实你就可以写一个十分简单的测试例子了,建议结合别人的测试代码进行学习,如https://github.com/larvalabs/cryptopunks/tree/master/test
在下面进行测试:
1)一开始,当我想要直接复制truffle中写好的测试文件,进行小部分更改直接进行使用时,发现出错:
truffle中只需要这两句话就可以部署好合约,但是在没框架的情况下是不可以的
var testToken = artifacts.require(“./test-punk.sol”);
instance = await testToken.deployed(50);
这样会报错:
1.ReferenceError: artifacts is not defined
2.(function (exports, require, module, __filename, __dirname) { pragma solidity ^0.4.20;
^^^^^^^^
SyntaxError: Unexpected identifier
所以这就说明了在框架外是不可以这样子进行合约的编译的
通过上面我们就能够知道truffle到使用上面两句部署指令前,还进行了编译compile和部署migrate,所以在测试前要将合约的编译和部署都弄好,你可以通过查看我写的remix的使用来学怎么使用remix进行编译和部署或者是看nodejs部署智能合约的方法来自己编写代码进行编译和部署,最终得到合约的部署地址NFMAddress
部署成功后,之后如果想在别的地方进行使用,需要以下几句话句话:
const NFMAbi = require("./testToken.json");//合约生成的Abi,一般为json文件
const NFMContract = web3.eth.contract(NFMAbi);
const instance = NFMContract.at(NFMAddress);
此时就能够调用该函数中的函数及变量了
2)其次,还要记得将相应的模块包下载下来,这里要添加package.json文件配置等内容
Error: Cannot find module ‘web3'
你安装的web3一定要放在本地的node_modules文件夹下,不然是读不出来的
3)使用nodejs来进行合约的编译和部署时,发现出现下面的错误
1.let abi = compiledContract.contracts['testToken'].interface;
出错:
TypeError: Cannot read property 'interface' of undefined
然后输出compiledContract进行查看
2.console.log(compiledContract);
发现在编译处就出错了
{ contracts: {},
errors:
[ ':49:17: ParserError: Expected identifier, got \'LParen\'\n constructor (uint number) public{\n ^\n' ],
sourceList: [ '' ],
sources: {} }
还有:
{ contracts: {},
errors:
[ ':26:9: TypeError: Wrong argument count for function call: 2 arguments given but expected 1.\n require(propertyValueToOwner[propertyValue] == 0x0,\'this is not the first-sell\');\n ^------------------------------------------------------------------------------^\n',
发现可能是版本的问题,因为这里声明构造函数使用了新的声明方式,但是在这里没能被识别出。所以下载了新版本的solc,然后就成功了
4)
contract('testToken',async (accounts) => {
出错:ReferenceError: contract is not defined
因为contract是truffle框架弄的,不在框架中是不能够这样使用的,这时候想要使用账号只能老实地连接区块链,通过web3模块去调用API接口
var Web3 = require("web3");
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8201"));
account1 = web3.eth.accounts[0];
5)最后运行结果也有问题:
用户deMBP:testToken 用户$ mocha test-mocha.js
testToken Test
check tokenNumber
1) deploy contract
2) sell token
3) buy token
4) check the balance
5) withdrawl the balance
0 passing (118ms)
5 failing
1) testToken Test
deploy contract:
TypeError: Cannot read property 'call' of undefined
at Context.<anonymous> (test-mocha.js:56:43)
2) testToken Test
sell token:
TypeError: Cannot read property 'sellToken' of undefined
at Context.<anonymous> (test-mocha.js:77:19)
3) testToken Test
buy token:
TypeError: Cannot read property 'testTokenIdToOwner' of undefined
at Context.<anonymous> (test-mocha.js:90:25)
4) testToken Test
check the balance:
TypeError: Cannot read property 'pendingDrawalOfUser' of undefined
at Context.<anonymous> (test-mocha.js:112:38)
5) testToken Test
withdrawl the balance:
TypeError: Cannot read property 'pendingDrawalOfUser' of undefined
at Context.<anonymous> (test-mocha.js:124:31)
Contract mined! address: 0x3cb4464f73eda60ac3ba1d46cd0544cd7ae18040 transactionHash: 0x62f27e3ccdfcb6ba54547107bbc8f1f9f152f0f588eddec9b1dc3a65d1d7d047
后面发现这个原因是部署回调得到instance前,it函数中的内容就已经开始调用了,这样的话怎么着instance都是undefined的,那么肯定是不可能能够调用合约中的函数的。而且在这里将部署函数放在了一个单独的it测试用例当中,但是下面的测试用例都是应该等待部署完成后才能够测试成功的,那么这样就不可能成功测试了,因为it测试用例是同步进行的,在部署的同时,其他测试用例也都开始运行了,⚠️有前后关系的测试内容应该要写在一个测试用例当中。
所以就不做在这之前进行部署的事情了,毕竟我们肯定在测试之前是已经把合约部署上去的了,那么只要用
let instance = MyContract.at('0x86757c9bdea10815e7d75a1577b6d9d2825dae0a');
这句话就行了
6)然后再运行也出错:
mocha Error: Timeout of 2000ms exceeded.
则需要你再运行的时候添加
mocha -t 20000 test.js
因为其默认的时间是2000ms,你的时间如果过大,可以进行自己设置
7)当你使用describe这些mocha的形式语句的时候,使用node test.js是不能运行成功的,会返回错误:
用户MBP:testToken 用户$ node test.js
/Users/用户/testToken/test.js:165
describe('testToken',function(){
^
ReferenceError: describe is not defined
所以一定要用mocha开头,mocha test.js
实现:
const debug = require("debug")("testToken"); const assert = require('assert') // var testToken = require("./test-punk.sol"); const Web3 = require('web3'); const web3 = new Web3(); web3.setProvider(new Web3.providers.HttpProvider('http://127.0.0.1:7545')); const fs = require("fs"); const solc = require("solc"); let source = fs.readFileSync("testToken.sol",'utf8');//read file let compiledContract = solc.compile(source,1);//compile // console.log(compiledContract); for (let contractName in compiledContract.contracts) { console.log("in"); var bytecode = compiledContract.contracts[contractName].bytecode; var abi = JSON.parse(compiledContract.contracts[contractName].interface); //将abi写成json形式 console.log("out"); } // 你要测试的是接口 // console.log(bytecode); // console.log(abi); let gasEstimate = web3.eth.estimateGas({data:'0x'+bytecode}); let MyContract = web3.eth.contract(abi); debug("deploying contract"); // let instance; let instance = MyContract.at('0x86757c9bdea10815e7d75a1577b6d9d2825dae0a');//可改 var user1 = web3.eth.accounts[0]; console.log(user1); var user2 = web3.eth.accounts[1]; var user3 = web3.eth.accounts[2]; var user4 = web3.eth.accounts[3]; var user5 = web3.eth.accounts[4]; var propertyValues = ['0x00000001','0x00000002','0x00000003','0x00000004','0x00000005']; var prices = [11,12,13,14,15]; var users = [user1,user2,user3,user4,user5]; //这个地方是想要在这里同时部署合约,但是发现总是不成功,部署异步总是太慢,before()函数好像也没有什么用,所以后面我也只能放弃部署了,只能先部署完得到地址再来调用了 // function deployContract() { // instance = MyContract.new(50,{from:user1,data:'0x'+bytecode,gas:47000000},function(e,contract){ // if(typeof contract.address !== 'undefined'){ // console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash); // } // }); // } // before(deployContract); // var instance; describe("testToken Test",function(){ // it("deploy contract",async function(){ it("contract begin",async function(){ let num = propertyValues.length; // instance = await testToken.deployed(50); console.log(instance.address); console.log('check tokenNumber'); console.log(await instance.tokenNumber.call()); debug("create token");
//首先先创建5个token for(let i=0; i<num; i++){ await instance.create(propertyValues[i],prices[i],{from:users[i],value:prices[i],gas:30000000}); } debug("display token");
//然后查看生成的token的属性 let nextTokenId = await instance.nextTokenToCreate.call(); console.log("nextTokenId is :"+ nextTokenId); if(parseInt(nextTokenId) != 5){ console.log("initial token failed"); }else{ for(let i =0; i<num; i++){ console.log(await instance.propertyOfToken.call(i)); } } }); //然后sell token it("sell token",async function(){ await instance.sellToken(0,21,{from:user1,gas:30000000}); await instance.sellToken(1,22,{from:user2,gas:30000000}); //查看是否成功sell console.log(await instance.tokenIdToSell.call(0)); console.log(await instance.tokenIdToSell.call(1)); }); //其他用户对sell的token进行购买 it("buy token",async function(){
//查看购买前token的拥有者是谁 console.log(instance.testTokenIdToOwner.call(0)); console.log(instance.testTokenIdToOwner.call(1)); await instance.buyToken(0,{from:user3,value:21,gas:30000000}); await instance.buyToken(1,{from:user4,value:22,gas:30000000}); let buyer1 = await instance.testTokenIdToOwner.call(0); let buyer2 = await instance.testTokenIdToOwner.call(1); //查看购买后的拥有者是谁来核实购买成功进行 console.log(buyer1); console.log(buyer2); console.log("check the sell after buying"); console.log(await instance.tokenIdToSell.call(0)); console.log(await instance.tokenIdToSell.call(1)); assert.equal(buyer1,user3,"buying 0 is failed"); assert.equal(buyer2,user4,"buying 1 is failed"); }); //查看用户临时账户中此时有多少积蓄 it("check the balance",async function(){ let user1Balance = await instance.pendingDrawalOfUser.call(user1); let user2Balance = await instance.pendingDrawalOfUser.call(user2); console.log(user1Balance); console.log(user2Balance); assert.equal(user1Balance,21,"user1 balance number is not right"); assert.equal(user2Balance,22,"user1 balance number is not right"); }); //然后将临时账户中的钱转到钱包中 it("withdrawl the balance",async function(){ console.log(await instance.pendingDrawalOfUser.call(user1)); console.log(await instance.pendingDrawalOfUser.call(user2)); await instance.withdrawl({from:user1,gas:30000000}); await instance.withdrawl({from:user2,gas:30000000}); console.log(await instance.pendingDrawalOfUser.call(user1)); console.log(await instance.pendingDrawalOfUser.call(user2)); }); }); 结果是: 用户deMBP:testToken 用户$ mocha test-mocha.js in out 0x3455f15cc11f2e77c055f931a6c918ccc7c18fd8 testToken Test 0x86757c9bdea10815e7d75a1577b6d9d2825dae0a check tokenNumber BigNumber { s: 1, e: 1, c: [ 50 ] } nextTokenId is :5
//生成的token的属性 [ BigNumber { s: 1, e: 0, c: [ 0 ] }, '0x0000000100000000000000000000000000000000000000000000000000000000', '0x3455f15cc11f2e77c055f931a6c918ccc7c18fd8', BigNumber { s: 1, e: 1, c: [ 11 ] } ] [ BigNumber { s: 1, e: 0, c: [ 1 ] }, '0x0000000200000000000000000000000000000000000000000000000000000000', '0x7ddad6a67544efb0c51808c77009a7b98cc81630', BigNumber { s: 1, e: 1, c: [ 12 ] } ] [ BigNumber { s: 1, e: 0, c: [ 2 ] }, '0x0000000300000000000000000000000000000000000000000000000000000000', '0xe9478ebcf4c755ad945a351261c8fa046672963b', BigNumber { s: 1, e: 1, c: [ 13 ] } ] [ BigNumber { s: 1, e: 0, c: [ 3 ] }, '0x0000000400000000000000000000000000000000000000000000000000000000', '0x920f422b761976972a9eadbec1f5341a9747ea6a', BigNumber { s: 1, e: 1, c: [ 14 ] } ] [ BigNumber { s: 1, e: 0, c: [ 4 ] }, '0x0000000500000000000000000000000000000000000000000000000000000000', '0xa17a7fa74a7dd57dff005b45234292e7daaf150c', BigNumber { s: 1, e: 1, c: [ 15 ] } ] ✓ contract begin (1541ms)
// [ true, '0x0000000100000000000000000000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 0 ] }, '0x3455f15cc11f2e77c055f931a6c918ccc7c18fd8', BigNumber { s: 1, e: 1, c: [ 21 ] } ] [ true, '0x0000000200000000000000000000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 1 ] }, '0x7ddad6a67544efb0c51808c77009a7b98cc81630', BigNumber { s: 1, e: 1, c: [ 22 ] } ] ✓ sell token (663ms) 0x3455f15cc11f2e77c055f931a6c918ccc7c18fd8 0x7ddad6a67544efb0c51808c77009a7b98cc81630 0xe9478ebcf4c755ad945a351261c8fa046672963b 0x920f422b761976972a9eadbec1f5341a9747ea6a check the sell after buying [ false, '0x0000000100000000000000000000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 0 ] }, '0x0000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 0 ] } ] [ false, '0x0000000200000000000000000000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 1 ] }, '0x0000000000000000000000000000000000000000', BigNumber { s: 1, e: 0, c: [ 0 ] } ] ✓ buy token (954ms) BigNumber { s: 1, e: 1, c: [ 21 ] } BigNumber { s: 1, e: 1, c: [ 22 ] } ✓ check the balance (206ms) BigNumber { s: 1, e: 1, c: [ 21 ] } BigNumber { s: 1, e: 1, c: [ 22 ] } BigNumber { s: 1, e: 0, c: [ 0 ] } BigNumber { s: 1, e: 0, c: [ 0 ] } ✓ withdrawl the balance (631ms) 5 passing (4s)
注意:在这里看好像这个例子也能够顺序执行,但是后面发现上面例子的所以it应该合成一个it来写,不然顺序是不一定能保证的,因为it测试用例在运行时是同步运行的,但是我这里的测试用例其实是有希望它按照顺序来运行,所以改为:
describe("testToken Test",function(){ it("contract begin",async function(){ let num = propertyValues.length; // instance = await testToken.deployed(50); console.log(instance.address); console.log('check tokenNumber'); console.log(await instance.tokenNumber.call()); debug("create token"); //首先先创建5个token for(let i=0; i<num; i++){ await instance.create(propertyValues[i],prices[i],{from:users[i],value:prices[i],gas:30000000}); } debug("display token"); //然后查看生成的token的属性 let nextTokenId = await instance.nextTokenToCreate.call(); console.log("nextTokenId is :"+ nextTokenId); if(parseInt(nextTokenId) != 5){ console.log("initial token failed"); }else{ for(let i =0; i<num; i++){ console.log(await instance.propertyOfToken.call(i)); } } //然后sell token await instance.sellToken(0,21,{from:user1,gas:30000000}); await instance.sellToken(1,22,{from:user2,gas:30000000}); //查看是否成功sell console.log(await instance.tokenIdToSell.call(0)); console.log(await instance.tokenIdToSell.call(1)); //其他用户对sell的token进行购买 //查看购买前token的拥有者是谁 console.log(instance.testTokenIdToOwner.call(0)); console.log(instance.testTokenIdToOwner.call(1)); await instance.buyToken(0,{from:user3,value:21,gas:30000000}); await instance.buyToken(1,{from:user4,value:22,gas:30000000}); let buyer1 = await instance.testTokenIdToOwner.call(0); let buyer2 = await instance.testTokenIdToOwner.call(1); //查看购买后的拥有者是谁来核实购买成功进行 console.log(buyer1); console.log(buyer2); console.log("check the sell after buying"); console.log(await instance.tokenIdToSell.call(0)); console.log(await instance.tokenIdToSell.call(1)); assert.equal(buyer1,user3,"buying 0 is failed"); assert.equal(buyer2,user4,"buying 1 is failed"); //查看用户临时账户中此时有多少积蓄 let user1Balance = await instance.pendingDrawalOfUser.call(user1); let user2Balance = await instance.pendingDrawalOfUser.call(user2); console.log(user1Balance); console.log(user2Balance); assert.equal(user1Balance,21,"user1 balance number is not right"); assert.equal(user2Balance,22,"user1 balance number is not right"); //然后将临时账户中的钱转到钱包中 console.log(await instance.pendingDrawalOfUser.call(user1)); console.log(await instance.pendingDrawalOfUser.call(user2)); await instance.withdrawl({from:user1,gas:30000000}); await instance.withdrawl({from:user2,gas:30000000}); console.log(await instance.pendingDrawalOfUser.call(user1)); console.log(await instance.pendingDrawalOfUser.call(user2)); }); });