Test and Deploy with Truffle - cogeorg/teaching GitHub Wiki
Using JavaScript as a testing framework will let you test your smart contract by simulating external client interactions. For that purpose, truffle integrates Mocha as JavaScript testing framework and Chai as a behavior driven development assertion library.
Recall the ExampleCoin contract.
pragma solidity ^0.5.0;
contract ExampleCoin {
address public minter;
mapping (address => uint) public balances;
event Sent(address from, address to, uint amount);
constructor() public {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
require(msg.sender == minter, "Function can only be called by minter.");
require(amount < 1e60, "Amount is too high.");
balances[receiver] += amount;
}
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}
Note that this coin does not follow any ERC standard (see e.g. here). Let us create a truffle project for the contract written above:
$ mkdir ExampleCoin
$ cd ExampleCoin/
$ truffle init
Create a file called ExampleCoin.sol
inside the contracts/ folder and copy the above defined contract to that file. Also, create a file called 2_deploy_contracts.js
in the migrations/ folder and paste the following code:
var ExampleCoin = artifacts.require('./ExampleCoin.sol')
module.exports = function (deployer) {
deployer.deploy(ExampleCoin)
}
To recap the Truffle folder structure, have a look here.
Testing
In the test/ folder, create a file called ExampleCoin.js
. We want to test the following aspects:
- the minter is set to the address deploying the contract
- all addresses have zero coins on deployment
- minting works when the function is called from the minter address
- minting does not work when the function is called from any other address
- sending coins works
Here is the test file:
// import the contract artifact
const ExampleCoin = artifacts.require('./ExampleCoin.sol')
// test starts here
contract('ExampleCoin', function (accounts) {
// predefine the contract instance
let ExampleCoinInstance
// before each test, create a new contract instance
beforeEach(async function () {
ExampleCoinInstance = await ExampleCoin.new()
})
// first test: define what it should do in the string
it('should set account 0 to the minter', async function () {
// minter is a public variable in the contract so you can get it directly via the created call function
let minter = await ExampleCoinInstance.minter()
// check whether minter is equal to account 0
assert.equal(minter, accounts[0], "minter wasn't properly set")
})
// second test
it('should initialize with 0 coins in account all accounts', async function () {
// loop over the accounts
for (let i = 0; i < 10; i++) {
// get the balance of this account
let balance = await ExampleCoinInstance.balances(accounts[i])
// check that the balance is 0
assert.equal(balance.toNumber(), 0, "0 wasn't in account" + i.toString())
}
})
// third test
it('should mint 10 coins if sent from account 0', async function () {
// call the mint function to mint 10 coins to account 0
await ExampleCoinInstance.mint(accounts[0], 10, { 'from': accounts[0] })
// retrieve the updated balance of account 0
let balance = await ExampleCoinInstance.balances(accounts[0])
// check that the balance is now 10
assert.equal(balance.toNumber(), 10, "10 wasn't in account 0")
})
// fourth test
it('should send 5 coins from account 0 to account 1', async function () {
// call the mint function to mint 10 coins to account 0
await ExampleCoinInstance.mint(accounts[0], 10, { 'from': accounts[0] })
// send 5 coins from account 0 to account 1
await ExampleCoinInstance.send(accounts[1], 5, { 'from': accounts[0] })
// retrieve the balances of account 0 and account 1
let balance0 = await ExampleCoinInstance.balances(accounts[0])
let balance1 = await ExampleCoinInstance.balances(accounts[1])
// check that both balances are equal to 5
assert.equal(balance0.toNumber(), 5, "5 wasn't in account 0")
assert.equal(balance1.toNumber(), 5, "5 wasn't in account 1")
})
// more tests here
})
Of course, this test file is not complete. For example, you need to test the behavior of your contract when you send coins from an address that does not hold the necessary funds.
To run your test, open a shell, navigate to your project directory, and run
$ truffle test
revert
Testing If you remember, we required the minter of coins to be the owner of the contract. No other address than the one the deployed the contract should be able to mind coins. Otherwise, the mint
functions should revert. Let's test for that.
Testing for revert
is not straight forward but requires an extra package. Go ahead and install it in your project's directory via
$ npm install truffle-assertions
Next, in your test file ExampleCoin.js
, import the newly install package by adding the following line at the top:
const truffleAssert = require('truffle-assertions')
Finally, add the following test where it says //more tests here
:
// fifth test
it('should mint no coins if sent from account 1', async function () {
// try to mint 10 coins from using account 1
await truffleAssert.reverts(ExampleCoinInstance.mint(accounts[1], 10, { 'from': accounts[1] }))
// fetch the balance of account 1
let balance = await ExampleCoinInstance.balances(accounts[1])
// check that the balance of account 1 is still 0
assert.equal(balance.toNumber(), 0, "0 wasn't in account 1")
})
Now run your test again and you should see the following output:
Using network 'test'.
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Contract: ExampleCoin
✓ should set account 0 to the minter
✓ should initialize with 0 coins in account all accounts (198ms)
✓ should mint 10 coins if sent from account 0 (53ms)
✓ should send 5 coins from account 0 to account 1 (138ms)
✓ should mint no coins if sent from account 1 (58ms)
5 passing (736ms)
Deploying to Ganache
Once you are certain that your contract behaves the way you intended, you can deploy it. First, we need to configure the truffle-config.js
file. When you open it, you will see that it already contains a bunch of useful information, that is completely commented out. Take your time and read through the comments because they explain all the parts quite nicely.
Now, add your Ganache network in the networks
object:
networks: {
// ...
ganache: {
host: "127.0.0.1", // Localhost
port: 7545, // Standard Ganache port
network_id: "*", // Any network
} // you may have to add a , here
// ...
}
For now, we will only deploy to Ganache. Therefore, the host is 127.0.0.1
or localhost
. Different nodes and emulators listen to different ports by default:
- Ganache: 7545
- Truffle develop: 9545
- Public and private node: whatever you specified in the
genesis.json
file
Make sure that Ganache is running. Then, in your terminal, run the deploy command:
$ truffle migrate
To redeploy the contract after altering it, run
$ truffle migrage --compile-all --reset
compile-all
: forces a recompilation of all your solidity filesreset
: resets the state of the migrations contract and foresees a rerun of all the migration scripts
You may also want to use a specific network (if you have multiple defined). This is done via
$ truffle migrate --network <NETWORK-NAME-IN-CONFIG>
The terminal will print the following output:
Compiling your contracts...
===========================
> Compiling ./contracts/ExampleCoin.sol
> Compiling ./contracts/Migrations.sol
> Artifacts written to /home/sabine/git/teaching/ExampleCoin/build/contracts
> Compiled successfully using:
- solc: 0.5.12+commit.7709ece9.Emscripten.clang
Starting migrations...
======================
> Network name: 'ganache'
> Network id: 5777
> Block gas limit: 0x4a817c800
1_initial_migration.js
======================
Replacing 'Migrations'
----------------------
> transaction hash: 0x266e3f0fe765a066c84f9999f81fa4696b73aa3b543857eb68f0b16bfb81e246
> Blocks: 0 Seconds: 0
> contract address: 0xAf7695fFa9CEa21d36a0A5a6db6913f529ACA9ED
> block number: 1
> block timestamp: 1579184865
> account: 0xe0CA74697d8991352b0cb89AeA322D162898310F
> balance: 99.99472518
> gas used: 263741
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00527482 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00527482 ETH
2_deploy_contracts.js
=====================
Replacing 'ExampleCoin'
-----------------------
> transaction hash: 0x426e98e3f5980daa89860abefc4fdc3ae6fbc7ce5c867271adeb779862318cc2
> Blocks: 0 Seconds: 0
> contract address: 0xc1980F745A5E00109d965d02e127b96bFA63c4BB
> block number: 3
> block timestamp: 1579184866
> account: 0xe0CA74697d8991352b0cb89AeA322D162898310F
> balance: 99.98478228
> gas used: 455122
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00910244 ETH
> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00910244 ETH
Summary
=======
> Total deployments: 2
> Final cost: 0.01437726 ETH
Once you deployed your contract, you should see four transactions on the first account on Ganache:
- deployment of the
Migrations.sol
contract - call to the Migrations contract to update the last completed migration field of the Migrations contract
- deployment of the
ExampleCoin.sol
contract - call to the Migrations contract to update the last completed migration field Migrations contract
Deploying to a test network
In order to deploy to a test network like Ropsten or Rinkeby, we need a number of things:
- We need an account on the test network
- We need funds on that account
- We need an Infura API key
- We have to add the test network to the
truffle-config.js
file
An account on the test network
You actually already have an account on the test network. When you set up your MetaMask, it created an Account 1. This account is linked to all networks via the mnemonic, which is the sequence of words MetaMask wanted you to write down. It is used to derive your private key. Hence, all you need to do is open up MetaMask and change the network to "Rinkeby" and Account 1 is automatically your Rinkeby account.
Add funds to the account
Ropsten and Rinkeby both have faucets that will give you some test ether. The Rinkeby faucet wants you to put your address on social media to avoid spamming. You then paste the link to the social media post into the faucet and they will send you ether. The Ropsten faucet is easier. There, you just paste your address. However, since the network is slower, it takes longer to receive the test ether.
Whichever faucet you use, you should receive some test either in Account 1 on the corresponding network.
Get an Infura API key
Infura allows you to access any network, test or main, via an API. The advantage of this is that you don't have to sync an entire chain to deploy a contract. The disadvantage is, that you have to trust the Infura API. Hence, if you are just testing things, feel free to use Infura. If you are deploying production code, it may be worth it syncing the main chain and deploying it directly.
The said, let's go ahead and create an Infura API key. You will have to create an account first. Once you have an account, you can create a project.
Give it any name. I called mine "Test". Next, view the project.
Finally, retrieve your API key.
truffle-config.js
Add the test network to the First, you will have to uncomment the following lines
// const HDWalletProvider = require('truffle-hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
Add the Infura project ID as infuraKey
:
const HDWalletProvider = require('truffle-hdwallet-provider');
const infuraKey = "8f348cd258db4921a1ebf############";
const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();
Next, create a file called .secret
and paste your mnemonic words
fish ball exit run ...
Finally, add the network in the networks
object. Here are the examples for Ropsten:
ropsten: {
provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/${infuraKey}`),
network_id: 3, // Ropsten's id
gas: 5500000 // Ropsten has a lower block limit than mainnet
}
and Rinkeby:
rinkeby: {
provider: () => new HDWalletProvider(mnemonic, `https://rinkeby.infura.io/v3/${infuraKey}`),
network_id: 4 // Rinkeby's id
}
Deploy - Finally!
Switch back to the terminal and install the missing packages:
$ npm install truffle-hdwallet-provider fs
Now run the deploy command:
$ truffle deploy --network ropsten
(The command is equivalent for rinkeby.)