Unit Testing Solidity Smart Contracts
Hello, world! I consider unit testing one of the most important techniques and habits to create quality in applications from the very beginning. With the advent of Blockchain and Solidity smart-contracts, I keep the habit and continuously looking for improve it. In particular with Blockchain, I don't think the way to test a smart contract is through deploy it to Ganache, set breakpoints through the code of interest, printf-like debug, and tricks like that. There's nothing wrong with them except that this requires to develop a client application (say, an HTML page with web3.js) and, when testing is against a running Blockchain, it consumes more time and resources. Unit testing works better, is cheaper, and increases productivity while makes the smart contract more resilient to adapts and changes. This article is about using JavaScript to unit test a Solidity smart contract. It assumes you are familiarized with Truffle Framework, the Solidity programming language, Mocha, Chai, and Visual Studio Code. These are the versions of Truffle and Solidity I am using in this article:
Do you see the truffle test command as part of the cycle? That's what I am going to do with the MetaCoin smart contract:
As a matter of fact, the box contains unit tests written in JavaScript and Solidity under ./test. For the purposes of this article, I deleted both: the Solidity unit tests file and the JavaScript. I am going to walk you through writing JavaScript unit tests from scratch.
Let's start by creating the MetaCoin smart contract unit tests in ./test/metaCoin.js with the following initial content:
Since the MetaCoin smart contract is the subject under test, it needs to be referenced through const MetaCoin = artifacts.require("MetaCoin");. A smart contract test suite is defined by the contract function (just like the describe function does in typical JavaScript unit testing with Jasmine). Note the (accounts) => { }; function; is currently empty but I will talk about it later. At this point, let's start either Ganache or truffle develop and run the test suite. Although empty, it helps to get in touch with some technicalities I encountered with the truffle metacoin box. It happens that when the truffle test the following error occurs:
This error even happens with the original tests files I deleted but it's not a problem with them or the one just created. It has to do with the truffle-config.js. The unboxed version has the networks element commented out:
By uncommenting it...
...the test suite now runs without error:.
Back to the test suite and the (accounts) => { };function. The accounts argument holds a reference to the set of the accounts in the Blockchain network testing are targeted; in this case, Ganache. Let's modified the test suite to print them out...
Test suite output is below:
Which is, not surprisingly, the same in Ganache:
Or Truffle:
With those in hand, let's write unit tests to get balances for the first (index [0]) and last account (index [9]) like this:
Our test suite contains now two unit tests running successfully:
Once the MetaCoin smart contract has been deployed (await MetaCoin.deployed()) we are in position to call the getBalance method and verify that indeed accounts of interest have been initialized as intended. Now, let's check if metacoins transfer works:
And it does!
Well, that's it. As you can see, unit tests are pretty much the same from the Truffle MetaCoin box. In this article I only add a hint on how to fix a possible truffle test error and a light explanation of the (accounts) => { ... } parameter. The main takeaway is that old, well-established practices and disciplines like unit testing are applicable and provide the same benefits to new technologies like Blockchain.
PS C:\Blockchain> truffle version Truffle v5.0.1 (core: 5.0.1) Solidity v0.5.0 (solc-js) Node v8.9.3 PS C:\Blockchain>
A simple Solidity smart contract
Let's start by creating a MetaCoin smart contract (one of the truffle boxes):PS C:\Blockchain> MD MetaCoin PS C:\Blockchain> CD MetaCoin PS C:\Blockchain\MetaCoin> truffle unbox MetaCoin PS C:\Blockchain\MetaCoin> √ Preparing to download √ Downloading √ Cleaning up temporary files √ Setting up box Unbox successful. Sweet! Commands: Compile contracts: truffle compile Migrate contracts: truffle migrate Test contracts: truffle test PS C:\Blockchain\MetaCoin>
pragma solidity >=0.4.25 <0.6.0; import "./ConvertLib.sol"; // This is just a simple example of a coin-like contract. // It is not standards compatible and cannot be expected to talk to other // coin/token contracts. If you want to create a standards-compliant // token, see: https://github.com/ConsenSys/Tokens. Cheers! contract MetaCoin { mapping (address => uint) balances; event Transfer(address indexed _from, address indexed _to, uint256 _value); constructor() public { balances[tx.origin] = 10000; } function sendCoin(address receiver, uint amount) public returns(bool sufficient) { if (balances[msg.sender] < amount) return false; balances[msg.sender] -= amount; balances[receiver] += amount; emit Transfer(msg.sender, receiver, amount); return true; } function getBalanceInEth(address addr) public view returns(uint){ return ConvertLib.convert(getBalance(addr),2); } function getBalance(address addr) public view returns(uint) { return balances[addr]; } }
Let's start by creating the MetaCoin smart contract unit tests in ./test/metaCoin.js with the following initial content:
const MetaCoin = artifacts.require("MetaCoin"); contract('MetaCoin', (accounts) => { });
PS C:\Blockchain\MetaCoin> truffle test Error: The network id specified in the truffle config (4447) does not match the one returned by the network (5777). Ensure that both the network and the provider are properly configured. at detectNetworkId (C:\Users\FranciscoJavier\AppData\Roaming\npm\node_modules\truffle\build\webpack:\packages\truffle-core\lib\environment.js:71:1) at <anonymous> at process._tickCallback (internal/process/next_tick.js:188:7) Truffle v5.0.1 (core: 5.0.1) Node v8.9.3 PS C:\Blockchain\MetaCoin>
module.exports = { // Uncommenting the defaults below // provides for an easier quick-start with Ganache. // You can also follow this format for other networks; // see <http://truffleframework.com/docs/advanced/configuration> // for more details on how to specify configuration options! /* networks: { development: { host: "127.0.0.1", port: 7545, network_id: "*" }, test: { host: "127.0.0.1", port: 7545, network_id: "*" } */ } };
module.exports = { // Uncommenting the defaults below // provides for an easier quick-start with Ganache. // You can also follow this format for other networks; // see <http://truffleframework.com/docs/advanced/configuration> // for more details on how to specify configuration options! networks: { development: { host: "127.0.0.1", port: 7545, network_id: "*" }, test: { host: "127.0.0.1", port: 7545, network_id: "*" } } };
PS C:\Blockchain\MetaCoin> truffle test Using network 'development'. Compiling .\contracts\ConvertLib.sol... 0 passing (5ms) PS C:\Blockchain\MetaCoin>
const MetaCoin = artifacts.require("MetaCoin"); contract('MetaCoin', (accounts) => { for (var i = 0; i < accounts.length; i++) { console.log(`Account[${i}] = ${accounts[i]}`); } });
PS C:\Blockchain\MetaCoin> truffle test Using network 'development'. Compiling .\contracts\ConvertLib.sol... Account[0] = 0xd29cf8Cf94cF21692167f4fA946BC8F47DA45324 Account[1] = 0x38e9b32e96A8D189e7A7E815F5c7D817aAf738E0 Account[2] = 0x07CD88cCB00d257D7a577C214aa1deaCc3AE9364 Account[3] = 0x521842D914F9Cb5145d87294B1388603B4916F57 Account[4] = 0xE7E9ddcf36C18fC919551c83e1286Ae2Ec20FCb0 Account[5] = 0xe70435BC5F5eD77EB5C35D30218Dd30998417fFd Account[6] = 0x3ce5E55CFee669fF339c45872A4feffdf6dE0fF8 Account[7] = 0xa5721c38A1011bb8B7bB3b9733933743191cFdD2 Account[8] = 0x823f6f292F4934994D9650f1c2278Bc073edcA42 Account[9] = 0xCA8665dc5a34DA561913482fF3BdA398F0512373 0 passing (2ms) PS C:\Blockchain\MetaCoin>
Or Truffle:
PS C:\Blockchain\MetaCoin> truffle develop Truffle Develop started at http://127.0.0.1:7545/ Accounts: (0) 0xd29cf8Cf94cF21692167f4fA946BC8F47DA45324 (1) 0x38e9b32e96A8D189e7A7E815F5c7D817aAf738E0 (2) 0x07CD88cCB00d257D7a577C214aa1deaCc3AE9364 (3) 0x521842D914F9Cb5145d87294B1388603B4916F57 (4) 0xE7E9ddcf36C18fC919551c83e1286Ae2Ec20FCb0 (5) 0xe70435BC5F5eD77EB5C35D30218Dd30998417fFd (6) 0x3ce5E55CFee669fF339c45872A4feffdf6dE0fF8 (7) 0xa5721c38A1011bb8B7bB3b9733933743191cFdD2 (8) 0x823f6f292F4934994D9650f1c2278Bc073edcA42 (9) 0xCA8665dc5a34DA561913482fF3BdA398F0512373
const MetaCoin = artifacts.require("MetaCoin"); contract('MetaCoin', (accounts) => { it('should get a balance of 10000 metacoins for the first account', async () => { // arrange const instance = await MetaCoin.deployed(); // act const actual = (await instance.getBalance.call(accounts[0])).toNumber(); // assert const expected = 10000; assert.equal(expected, actual); }); it('should get a balance of 0 metacoins for the last account', async () => { // arrange const instance = await MetaCoin.deployed(); // act const actual = (await instance.getBalance.call(accounts[9])).toNumber(); // assert const expected = 0; assert.equal(expected, actual); }); });
PS C:\Blockchain\MetaCoin> truffle test Using network 'development'. Compiling .\contracts\ConvertLib.sol... Contract: MetaCoin √ should get a balance of 10,000 metacoins for the first account (63ms) √ should get a balance of 0 metacoins for the last account (78ms) 2 passing (250ms) PS C:\Blockchain\MetaCoin>
const MetaCoin = artifacts.require("MetaCoin"); contract('MetaCoin', (accounts) => { it('should get a balance of 10000 metacoins for the first account', async () => { // arrange const instance = await MetaCoin.deployed(); // act const actual = (await instance.getBalance.call(accounts[0])).toNumber(); // assert const expected = 10000; assert.equal(expected, actual); }); it('should get a balance of 0 metacoins for the last account', async () => { // arrange const instance = await MetaCoin.deployed(); // act const actual = (await instance.getBalance.call(accounts[9])).toNumber(); // assert const expected = 0; assert.equal(expected, actual); }); it('should transfer metacoins between accounts', async () => { // arrange const instance = await MetaCoin.deployed(); const acct0 = accounts[0]; const acct1 = accounts[1]; const amount = 123; let acct0InitialBalance; let acct0FinalBalance; let acct1InitialBalance; let acct1FinalBalance; acct0InitialBalance = (await instance.getBalance.call(acct0)).toNumber(); acct1InitialBalance = (await instance.getBalance.call(acct1)).toNumber(); // act await instance.sendCoin(acct1, amount, { from: acct0 }); // assert acct0FinalBalance = (await instance.getBalance.call(acct0)).toNumber(); acct1FinalBalance = (await instance.getBalance.call(acct1)).toNumber(); assert.equal(acct0FinalBalance, acct0InitialBalance - amount); assert.equal(acct1FinalBalance, acct1InitialBalance + amount); }); });
PS C:\Blockchain\MetaCoin> truffle test Using network 'development'. Compiling .\contracts\ConvertLib.sol... Contract: MetaCoin √ should get a balance of 10000 metacoins for the first account (62ms) √ should get a balance of 0 metacoins for the last account (93ms) √ should transfer metacoins between accounts (579ms) 3 passing (797ms) PS C:\Blockchain\MetaCoin>
I simply couldn’t depart your site before suggesting that I really enjoyed the usual information an individual supply in your visitors? Is going to be again steadily to check out new posts.
ReplyDeleteSoftware Testing Services
Software Testing Services in USA
Software Testing Companies in USA
Software Testing Services in India
Software Testing Companies
Software Testing Services Company
Functional Testing Services
Performance Testing Services
Test Automation Services
Security Testing Services
API Testing Services