
我要感谢我的同事:Sergey Nemesh,Mikhail Popsuyev,Evgeny Babich和Igor Titarenko的咨询,反馈和测试。 我还要感谢PolySwarm团队开发了Perigord的原始版本。
这是我第一篇中英文文章的翻译
尽管不是最令人愉快的测试,但是测试一直是软件开发的组成部分。 当涉及智能合约时,需要进行严格的测试,并特别注意细节,因为 部署到区块链网络后,错误将无法修复。 在过去的几年中,以太坊社区创建了许多用于开发智能合约的工具。 其中一些没有流行,例如Vyper-一种用于编写智能合约的Python方言。 其他,例如Solidity,已经成为公认的标准。 迄今为止,有关测试智能合约的最广泛的文档提供了许多Truffle&Ganache。 这两个工具都有很好的文档,关于Stack Overflow和类似资源的很多情况已经确定。 但是,这种方法有一个重要的缺点:要编写测试,您需要使用Node.js。
JavaScript陷阱
即使您不喜欢静态类型的编程语言并且喜欢JavaScript,也可以考虑打错字并开始比较使用不推荐使用的equal方法(而不是strictEqual)返回具有布尔值的字符串的函数的结果。
let proposalExists = await voting.checkProposal(); assert.equal(proposalExists, true, 'Proposal should exist');
如果checkProposal返回字符串“ yes”或“ no”,则您将始终将其转换为true。 动态类型隐藏了许多陷阱,即使是经验丰富的程序员,在处理大型项目或与其他开发人员进行团队合作时也可能犯这样的错误,他们可以更改代码而不报告代码。
Go的静态类型有助于防止此类错误。 此外,使用Go语言代替Node.js进行测试是任何开始使用智能合约的Go开发人员的梦想。
我的团队正在开发基于智能合约的投资系统,该系统具有非常复杂的体系结构。 智能合约系统包含超过2,000行代码。 由于团队的大部分是Go开发人员,因此在Go上进行测试比Node.js更可取。
第一个在Go上测试智能合约的环境
2017年,PolySwarm使用Go而不是JavaScript开发了Perigord (类似于Truffle的工具)。 不幸的是,该项目不再受支持,它只有一个教程和非常简单的示例。 此外,它不支持与Ganache集成(用于开发具有非常方便的GUI的以太坊的私有区块链)。 我们通过消除错误并引入了两个新功能来改进Perigord:从助记码生成钱包,并使用它们测试并连接到Ganache区块链。 您可以在此处阅读源代码。
原始的Perigord教程仅包含最简单的调用合同以更改单个值的示例。 但是,在现实世界中,您还需要从不同的钱包中拨打合同,发送和接收以太币等。 现在,您可以使用高级Perigord和老式的Ganache完成所有这些操作。 您将在下面找到有关使用Perigord&Ganache开发和测试智能合约的详细指南。
使用Advanced Perigord:完整指南
要使用Perigord,您需要安装Go 1.7 +,solc,abigen和Ganache。 请参阅您的操作系统的文档。
如下安装Perigord:
$ go get gitlab.com/go-truffle/enhanced-perigord $ go build
之后,您可以使用perigord命令:
$ perigord A golang development environment for Ethereum Usage: perigord [command] Available Commands: add Add a new contract or test to the project build (alias for compile) compile Compile contract source files deploy (alias for migrate) generate (alias for compile) help Help about any command init Initialize new Ethereum project with example contracts and tests migrate Run migrations to deploy contracts test Run go and solidity tests Flags: -h, --help help for perigord Use "perigord [command] --help" for more information about a command.
现在,我们将创建一个简单的Market智能合约,以演示可用的测试选项。
要启动项目,请在终端中输入以下内容:
$ perigord init market
该项目将出现在GOPATH的src /文件夹中。 如果要更改项目位置,请将项目移到另一个文件夹并更新导入路径。 让我们看看市场/文件夹中的内容。
$ tree . ├── contracts │ └── Foo.sol ├── generate.go ├── main.go ├── migrations │ └── 1_Migrations.go ├── perigord.yaml ├── stub │ ├── README.md │ └── main.go ├── stub_test.go └── tests └── Foo.go
与Truffle中创建的项目非常相似,不是吗? 但这一切都在进行中! 让我们看看perigord.yaml配置文件中的内容。
networks: dev: url: /tmp/geth_private_testnet/geth.ipc keystore: /tmp/geth_private_testnet/keystore passphrase: blah mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat num_accounts: 10
为了进行测试,您可以使用专用的geth网络和钱包文件,然后连接到Ganache。 这些选项是互斥的。 我们采用默认的助记符,生成10个帐户并连接到Ganache。 将perigord.yaml中的代码替换为:
networks: dev: url: HTTP://127.0.0.1:7545 mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat num_accounts: 10
HTTP http://127.0.0.1:7545-Ganache RPC服务器的标准地址。 请注意,您可以创建任意数量的帐户进行测试,但是只有在Ganache(GUI)中生成的帐户才会包含资金。
我们将创建一个名为Market.sol的合同。 他可以保存一对地址的记录,其中一个地址将资金发送到合同帐户,而另一个地址有权在合同持有人允许进行此类交易时收取资金。 例如,两个参与者互不信任,但是信任合同的所有者,合同的所有者决定是否满足特定条件。 该示例实现了几个基本功能以进行演示。
将联系人添加到项目:
$ perigord add contract Market
后缀.sol将自动添加。 您还可以添加其他合同或删除样本合同Foo.sol。 在GOPATH中工作时,可以使用导入合同来创建复杂的结构。 我们将拥有三个Solidity文件:主要的Market合同,Ownable和Migrations辅助合同以及SafeMath库。 您可以在此处找到源代码。
现在,该项目具有以下结构:
. ├── contracts │ ├── Market.sol │ ├── Ownable.sol │ └── SafeMath.sol ├── generate.go ├── main.go ├── migrations │ └── 1_Migrations.go ├── perigord.yaml ├── stub │ ├── README.md │ └── main.go ├── stub_test.go └── tests └── Foo.go
生成EVM字节码,ABI和Go绑定:
$ perigord build
添加将要部署的所有合同的迁移。 因为 我们只部署Market.sol,我们只需要进行一次新迁移:
$ perigord add migration Market
我们的合同不包含接受参数的构造函数。 如果需要将参数传递给构造函数,请将其添加到迁移文件中的Deploy {NewContract}函数中:
address, transaction, contract, err := bindings.Deploy{NewContract}(auth, network.Client(), “FOO”, “BAR”)
删除示例文件Foo.go并为我们的合同添加测试文件:
$ perigord add test Market
要使用确定性钱包,我们需要从配置文件中读取助记符:
func getMnemonic() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } mnemonic := viper.GetStringMapString("networks.dev")["mnemonic"] return mnemonic }
以下帮助程序函数用于获取网络地址:
func getNetworkAddress() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } networkAddr := viper.GetStringMapString("networks.dev")["url"] return networkAddr }
我们将需要的另一个辅助功能是sendETH,我们将使用它将以太币从生成的钱包之一(由索引表示)转移到任何以太坊地址:
func sendETH(s *MarketSuite, c *ethclient.Client, sender int, receiver common.Address, value *big.Int) { senderAcc := s.network.Accounts()[sender].Address nonce, err := c.PendingNonceAt(context.Background(), senderAcc) if err != nil { log.Fatal(err) } gasLimit := uint64(6721975) // in units gasPrice := big.NewInt(3700000000) wallet, err := hdwallet.NewFromMnemonic(getMnemonic()) toAddress := receiver var data []byte tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data) chainID, err := c.NetworkID(context.Background()) if err != nil { log.Fatal(err) } privateKey, err := wallet.PrivateKey(s.network.Accounts()[sender]) signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) if err != nil { log.Fatal(err) } ts := types.Transactions{signedTx} rawTx := hex.EncodeToString(ts.GetRlp(0)) var trx *types.Transaction rawTxBytes, err := hex.DecodeString(rawTx) err = rlp.DecodeBytes(rawTxBytes, &trx) err = c.SendTransaction(context.Background(), trx) if err != nil { log.Fatal(err) } }
以下两个函数用于修改合同调用:
func ensureAuth(auth bind.TransactOpts) *bind.TransactOpts { return &bind.TransactOpts{ auth.From, auth.Nonce, auth.Signer, auth.Value, auth.GasPrice, auth.GasLimit, auth.Context} } func changeAuth(s MarketSuite, account int) bind.TransactOpts { return *s.network.NewTransactor(s.network.Accounts()[account]) }
测试程序
对于通话,我们为特定合同创建一个contractSessionActual。 因为 合同有一个所有者,我们可以获取其地址并检查其是否与默认的零Ganache帐户匹配。 我们将按照以下步骤进行操作(我们将省略错误处理以节省空间):
contractSession := contract.Session("Market") c.Assert(contractSession, NotNil) contractSessionActual, ok := contractSession.(*bindings.MarketSession) c.Assert(ok, Equals, true) c.Assert(contractSessionActual, NotNil) owner, _ := contractSessionActual.Owner() account0 := s.network.Accounts()[0] c.Assert(owner.Hex(), Equals, account0.Address.Hex()) //Owner account is account 0
下一个有用的功能是更换引起合同的钱包:
ownerInd := 0 sender := 5 receiver := 6 senderAcc := s.network.Accounts()[sender].Address receiverAcc := s.network.Accounts()[receiver].Address //Call contract on behalf of its owner auth := changeAuth(*s, ownerInd) _, err = contractSessionActual.Contract.SetSenderReceiverPair(ensureAuth(auth), senderAcc, receiverAcc)
因为 测试中使用的主要功能之一是更改主叫合同,让我们代表发送方付款:
auth = changeAuth(*s, sender) //Change auth fo senderAcc to make a deposit on behalf of the sender client, _ := ethclient.Dial(getNetworkAddress()) //Let's check the current balance balance, _ := client.BalanceAt(context.Background(), contract.AddressOf("Market"), nil) c.Assert(balance.Int64(), Equals, big.NewInt(0).Int64()) //Balance should be 0 //Let's transfer 3 ETH to the contract on behalf of the sender value := big.NewInt(3000000000000000000) // in wei (3 eth) contractReceiver := contract.AddressOf("Market") sendETH(s, client, sender, contractReceiver, value) balance2, _ := client.BalanceAt(context.Background(), contract.AddressOf("Market"), nil) c.Assert(balance2.Int64(), Equals, value.Int64()) //Balance should be 3 ETH
完整的测试代码在这里 。
现在打开stub_test.go并确保所有导入都指向您当前的项目。 在我们的例子中是:
import ( _ "market/migrations" _ "market/tests" "testing" . "gopkg.in/check.v1" )
运行测试:
$ perigord test
如果一切都正确完成,那么在测试结束后,将会得到类似的结果:
Running migration 2 Running migration 3 OK: 1 passed PASS ok market 0.657s
如果有任何问题,请下载源文件并重复本指南中描述的步骤。
总结
Perigord是使用您喜欢的语言编写的可靠测试工具。 他创建了与Truffle相同的项目结构,并且拥有相同的团队,因此您无需重新学习。 静态类型和明确的函数签名使您可以快速开发和执行调试,并显着防止参数输入错误。 在Perigord中,您可以轻松地将现有项目迁移到Truffle(您要做的就是将合同文件复制并粘贴到相应的文件夹中并添加测试),还可以使用Go编写的测试来启动一个全新的项目。
我希望由PolySwarm团队开始并由Inn4Science继续进行的工作对Go社区有用,并使用不太方便的工具腾出大量的测试和调试时间。