πŸ“œ ⬆️ ⬇️

Ethereum Smart Contract Testing for Go: Goodbye JavaScript

image
I want to thank my colleagues: Sergey Nemesh, Mikhail Popsuev, Evgeny Babich and Igor Titarenko for their advice, feedback and testing. I also want to say thank you to the PolySwarm team for developing the original version of Perigord.

This is a translation of my first published article in Medium on English.


Testing has always been an integral part of software development, although not the most enjoyable. When it comes to smart contracts, thorough testing is needed with exceptional attention to detail, because errors will not be fixed after deployment in the blockchain network. In recent years, the Ethereum community has created many tools for developing smart contracts. Some of them did not become popular, for example, Vyper is a Python dialect for writing smart contracts. Others, such as Solidity, have become the recognized standard. The most extensive test documentation for smart contracts today is provided by the Truffle & Ganache bundle. Both of these tools have good documentation, many cases have already been solved on Stack Overflow and similar resources. However, this approach has one important drawback: Node.js should be used for writing tests.


JavaScript traps


Even if you are not a fan of static-typed programming languages ​​and like JavaScript, think about what you can make a typo and start comparing the result of executing a function that returns a string with a boolean value using the outdated equal method instead of strictEqual.


let proposalExists = await voting.checkProposal(); assert.equal(proposalExists, true, 'Proposal should exist'); 

If checkProposal returns the string β€œyes” or β€œno”, you always convert them to true. Dynamic typing hides many such pitfalls, and even experienced programmers can make similar mistakes while working on a large project or in a team with other developers who can make changes to the code and not report it.


Static typing in Go allows you to prevent such errors. In addition, the use of the Go language instead of Node.js for testing is the dream of any Go-developer who starts working with smart contracts.


My team has been developing an investment system based on smart contracts with a very complex architecture. The smart contract system contained over 2000 lines of code. Since the main part of the team were Go-developers, testing for Go was preferable to Node.js.


The first environment to test smart contracts on Go


In 2017, PolySwarm was developed by Perigord , a tool similar to Truffle, using Go instead of JavaScript. Unfortunately, this project is no longer supported, it has only one tutorial with very simple examples. In addition, it does not support integration with Ganache (a private blockchain for developing Ethereum with a very convenient GUI). We improved Perigord by eliminating bugs and introducing two new functions: generating wallets from the mnemonic code and using them to test and connect to the Ganache blockchain. You can view the source code here .


The original Perigord tutorial contains only the simplest example of calling a contract to change a single value. However, in the real world, you also need to call a contract from different wallets, send and receive Ether, etc. Now you can do all this using advanced Perigord and good old Ganache. Below you will find a detailed guide to developing and testing smart contracts with Perigord & Ganache.


Using Improved Perigord: The Complete Guide


To use Perigord, you need to install Go 1.7+, solc, abigen and ganache. Please read the documentation for your operating system.


Install Perigord as follows:


 $ go get gitlab.com/go-truffle/enhanced-perigord $ go build 

After that you can use the perigord command:


 $ 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. 

We will now create a simple Smart Market contract to demonstrate the available test options.


To start a project, enter the following into the terminal:


 $ perigord init market 

The project will appear in the src / folder of GOPATH. Move the project to another folder and update the import paths if you want to change its location. Let's see what is in the market / folder.


 $ 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 

Very similar to the project created in Truffle, is not it? But this is all on Go! Let's see what's in the perigord.yaml configuration file.


 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 

For testing, you can use both the geth private network and wallet files, as well as connect to Ganache. These options are mutually exclusive. We take the default mnemonic, generate 10 accounts and connect to Ganache. Replace the code in perigord.yaml with:


 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://127.0.0.1:7545 - the standard address of the server Ganache RPC. Please note that you can create as many accounts as you wish for testing, but only accounts generated in Ganache (GUI) will contain funds.


We will create a contract called Market.sol. He can keep a record of address pairs, one of which sends funds to the account of the contract, and the other has the right to receive funds when the owner of the contract gives permission for such a transaction. For example, two parties do not trust each other, but trust the contract owner, who decides whether a certain condition is met. The example implements several basic functions for demonstration purposes.


Add a contact to the project:


 $ perigord add contract Market 

Postfix .sol will be added automatically. You can also add other contracts or delete the example contract Foo.sol. While you work at GOPATH, you can use import contracts to create complex structures. We will have three Solidity files: the main Market contract, the Ownable and Migrations auxiliary contracts, and the SafeMath library. You can find the source code here .


Now the project has the following structure:


 . β”œβ”€β”€ 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 

We generate byte-code EVM, ABI and Go bindings:


 $ perigord build 

We add migrations of all contracts that you will deploy. Because we only deploy Market.sol, we need only one new migration:


 $ perigord add migration Market 

Our contract does not contain a constructor that accepts parameters. If you need to pass parameters to the constructor, add them to the Deploy {NewContract} function in the migration file:


 address, transaction, contract, err := bindings.Deploy{NewContract}(auth, network.Client(), β€œFOO”, β€œBAR”) 

Delete the sample file Foo.go and add a test file for our contract:


 $ perigord add test Market 

To use deterministic wallets, we need to read the mnemonic from the configuration file:


 func getMnemonic() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } mnemonic := viper.GetStringMapString("networks.dev")["mnemonic"] return mnemonic } 

The following auxiliary function is used to get the network address:


 func getNetworkAddress() string { viper.SetConfigFile("perigord.yaml") if err := viper.ReadInConfig(); err != nil { log.Fatal() } networkAddr := viper.GetStringMapString("networks.dev")["url"] return networkAddr } 

Another auxiliary function that we need is sendETH, we will use it to transfer Ether from one of the generated wallets (indicated by the index) to any Ethereum address:


 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) } } 

The following two functions are used to modify a contract call:


 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]) } 

Testing procedure


To call, we create a contractSessionActual for a specific contract. Because the contract has an owner, we can get its address and check whether it corresponds to the default zero Ganache account. We do this as follows (we omit error handling to save space):


 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 

The next useful feature is to change the wallet calling the contract:


 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) 

Because One of the main functions used in testing is changing the calling contract, let's make a payment on behalf of the sender:


 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 

The full test code is here .


Now open stub_test.go and make sure that all imports point to your current project. In our case it is:


 import ( _ "market/migrations" _ "market/tests" "testing" . "gopkg.in/check.v1" ) 

Run the tests:


 $ perigord test 

If everything is done correctly, after the end of testing there will be a similar result:


 Running migration 2 Running migration 3 OK: 1 passed PASS ok market 0.657s 

If you have problems, download the source files and repeat the steps in this guide.


Finally


Perigord is a robust testing tool written in your favorite language. It creates the same project structure as Truffle, and has the same commands, so you will not need to relearn. Static typing and an unambiguous signature of functions allow you to quickly develop and execute debugging, and also largely protect against typos in arguments. In Perigord, you can easily migrate an existing project to Truffle (all you need is to copy and paste the contract files into the appropriate folder and add tests), as well as start a completely new project with tests written in Go.


I hope that the work begun by the PolySwarm team and continued by Inn4Science will be useful for the Go community and free from hours of testing and debugging using less convenient tools.


')

Source: https://habr.com/ru/post/445254/


All Articles