Introduction
Teth is a Ethereum smart contract test framework in ruby.It provides two testing environments: testing in ruby EVM and testing in geth.You don't need to understand ruby grammar, just enjoy syntactic sugar.
Dependencies
- Solidity
- Ruby ~> 2.2.2
- ruby-bitcoin-secp256k1
- Go-ethereum ~> 1.4.11
Install
bundle install teth
How to use
Help command
$ teth
Usage: teth COMMAND [ARGS]
The most common teth commands are:
new Create a new Smart Contract application. "teth new my_app" creates a
new application called my_app in "./my_app" (short-cut alias: "n")
generate Generate new solidity smart contract and test files. "teth generate token"
creates Token contract and corresponding test files. (short-cut alias: "g")
test Run your ruby tests in ruby evm. (short-cut alias: "t")
init Bootstraps and initialises a new genesis block. "teth init" creates data directory
for private chain.(short-cut alias: "i")
import_keys Import keys to private chain (short-cut alias: "ik")
build Build contract (short-cut alias: "b")
migrate Deploy contract on private chain (short-cut alias: "m")
server Start geth server (short-cut alias: "s")
console Start geth attach (short-cut alias: "c")
gtest Run your javascript tests on geth (short-cut alias: "gt")
All commands can be run with -h (or --help) for more information.
Creeat project
Create a new Smart Contract application
$ teth n examples
Creating project examples...
Resolving dependencies...
Using ffi 1.9.14
Using little-plugger 1.1.4
Using multi_json 1.12.1
Using digest-sha3 1.1.0
Using ethash 0.2.0
Using fiddler-rb 0.1.2
Using lru_redux 1.1.0
Using minitest 5.9.0
Using rlp 0.7.3
Using bundler 1.12.5
Using bitcoin-secp256k1 0.4.0
Using logging 2.1.0
Using leveldb 0.1.9
Using block_logger 0.1.2
Using ruby-ethereum 0.9.5
Using teth 0.1.1
Bundle complete! 1 Gemfile dependency, 16 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.
Done.
$ cd examples
Generate new Smart Contract and test files
$ teth g token
Creating Token contract file...
Creating token test files...
Done.
This generates contract file contracts/Token.sol
, and test files tests/token_test.rb
, gtests/Token_test.js
.
Edit token contract and test file.
contracts/Token.sol
pragma solidity ^0.4.0;
contract Token {
address issuer;
mapping (address => uint) balances;
event Issue(address account, uint amount);
event Transfer(address from, address to, uint amount);
function Token() {
issuer = msg.sender;
}
function issue(address account, uint amount) {
if (msg.sender != issuer) throw;
balances[account] += amount;
Issue(account, amount);
}
function transfer(address to, uint amount) {
if (balances[msg.sender] < amount) throw;
balances[msg.sender] -= amount;
balances[to] += amount;
Transfer(msg.sender, to, amount);
}
function getBalance(address account) constant returns (uint) {
return balances[account];
}
}
tests/token_test.rb
require 'teth/minitest'
class TokenTest < Teth::Minitest
def test_contract_deployed
assert_equal false, contract.address.nil?
end
def test_issue_balance
assert_equal 0, contract.getBalance(bob)
contract.issue bob, 100
assert_equal 100, contract.getBalance(bob)
end
def test_issue_exception
assert_raises(TransactionFailed) do
contract.issue bob, 100, sender: eve_privkey
end
assert_equal 0, contract.getBalance(bob)
end
def test_token_transfer
contract.issue bob, 100
contract.transfer carol, 90, sender: bob_privkey
assert_equal 90, contract.getBalance(carol)
assert_raises(TransactionFailed) { contract.transfer carol, 90, sender: bob_privkey }
end
end
Run tests in ruby evm
$ teth t token
Test Token contract...
Run options: --seed 2192
# Running:
....
Finished in 1.935546s, 2.0666 runs/s, 3.6166 assertions/s.
4 runs, 7 assertions, 0 failures, 0 errors, 0 skips
Done.
Unit tests
You can wirte fast, simple tests.
require 'teth/minitest'
class TokenTest < Teth::Minitest
def test_contract_deployed
assert_equal false, contract.address.nil?
end
def test_issue_balance
assert_equal 0, contract.getBalance(bob)
contract.issue bob, 100
assert_equal 100, contract.getBalance(bob)
end
def test_issue_exception
assert_raises(TransactionFailed) do
contract.issue bob, 100, sender: eve_privkey
end
assert_equal 0, contract.getBalance(bob)
end
def test_token_transfer
contract.issue bob, 100
contract.transfer carol, 90, sender: bob_privkey
assert_equal 90, contract.getBalance(carol)
assert_raises(TransactionFailed) { contract.transfer carol, 90, sender: bob_privkey }
end
end
More details: https://github.com/seattlerb/minitest
Init geth block
$ teth init
Initialising a new genesis block...
***** Using geth at: geth
I0917 16:01:17.338908 ethdb/database.go:82] Alloted 16MB cache and 16 file handles to /Users/u2/cryptape/teth/examples/data/chaindata
I0917 16:01:17.347151 cmd/geth/main.go:299] successfully wrote genesis block and/or chain rule set: 611596e7979cd4e7ca1531260fa706093a5492ecbdf58f20a39545397e424d04
Import keys to geth
$ teth ik
Importing keys, this will take a while, please be patient......
***** Using geth at: geth
***** Import all pre-funded private keys
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/3ae88fe370c39384fc16da2c9e768cf5d2495b48.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {3ae88fe370c39384fc16da2c9e768cf5d2495b48}
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/81063419f13cab5ac090cd8329d8fff9feead4a0.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {81063419f13cab5ac090cd8329d8fff9feead4a0}
Notice: No need to input your password. The default password is 123456
spawn geth --datadir data account import ./private_keys/9da26fc2e1d6ad9fdd46138906b0104ae68a65d8.key
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {9da26fc2e1d6ad9fdd46138906b0104ae68a65d8}
***** Done.
Notice: This will take a while, please be patient. No need to input your password.
Build contract
$ teth build token
Building contract Token
======= Token =======
Gas estimation:
construction:
20201 + 73400 = 93601
external:
-------------------------------------
Enter Gas:
400000
Enter Value To Be Transferred:
Enter Input:
Done.
Build all contracts if no contract name provided.
Start geth server
$ teth server
***** Using geth at: geth
Start geth server...
I0917 16:17:16.882572 ethdb/database.go:82] Alloted 128MB cache and 1024 file handles to data/chaindata
I0917 16:17:16.894415 ethdb/database.go:169] closed db:data/chaindata
I0917 16:17:16.895446 ethdb/database.go:82] Alloted 128MB cache and 1024 file handles to data/chaindata
I0917 16:17:16.946341 eth/backend.go:621] upgrading db log bloom bins
I0917 16:17:16.946478 eth/backend.go:629] upgrade completed in 142.276µs
I0917 16:17:16.946513 ethdb/database.go:82] Alloted 16MB cache and 16 file handles to data/dapp
I0917 16:17:16.951072 eth/backend.go:172] Protocol Versions: [63 62], Network Id: 31415926
I0917 16:17:16.951142 eth/backend.go:201] Blockchain DB Version: 3
I0917 16:17:16.953641 core/blockchain.go:206] Last header: #0 [611596e7…] TD=131072
I0917 16:17:16.953667 core/blockchain.go:207] Last block: #0 [611596e7…] TD=131072
I0917 16:17:16.953678 core/blockchain.go:208] Fast block: #0 [611596e7…] TD=131072
I0917 16:17:16.954420 p2p/server.go:313] Starting Server
I0917 16:17:16.955324 p2p/server.go:556] Listening on [::]:30303
I0917 16:17:16.957427 node/node.go:296] IPC endpoint opened: data/geth.ipc
I0917 16:17:16.959797 node/node.go:366] HTTP endpoint opened: http://localhost:8545
I0917 16:17:17.945231 cmd/geth/accountcmd.go:189] Unlocked account 3ae88fe370c39384fc16da2c9e768cf5d2495b48
I0917 16:17:19.158064 p2p/nat/nat.go:111] mapped network port tcp:30303 -> 30303 (ethereum p2p) using UPNP IGDv1-IP1
Migrate
Deploy your contract on geth, must keep teth sever started.
$ teth m
Migrating contract token
***** Using geth at: geth
null
Contract transaction send: TransactionHash: 0x3a9ca7a774a4bc5b3ba23b57f3c65a5debbfcbba422f902009909604ee668a63 waiting to be mined...
Compiled Object : TokenCompiled
Contract : TokenContract
Contract Instance : Token
true
Contract mined! Address: 0x3a020580345e79e223580d8d6a50e063667f19b5
Done.
This deploys contract on geth, and creates two files.One is temp/db/Token.json
which keeps abi and address.
{
"Token": {
"abi": [
{
"constant": false,
"inputs": [
{
"name": "account",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"name": "issue",
"outputs": [
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "account",
"type": "address"
}
],
"name": "getBalance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"inputs": [
],
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "account",
"type": "address"
},
{
"indexed": false,
"name": "amount",
"type": "uint256"
}
],
"name": "Issue",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "from",
"type": "address"
},
{
"indexed": false,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "amount",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
],
"bin": "0x606060405260008054600160a060020a0319163317905561016f806100246000396000f3606060405260e060020a6000350463867904b48114610034578063a9059cbb1461005e578063f8b2cb4f14610092575b610002565b34610002576100bc600435602435600054600160a060020a0390811633909116146100be57610002565b34610002576100bc60043560243533600160a060020a03166000908152600160205260409020548190101561010f57610002565b3461000257600160a060020a03600435166000908152600160205260409020546060908152602090f35b005b600160a060020a03821660008181526001602052604090819020805484019055606091825260808390527fc65a3f767206d2fdcede0b094a4840e01c0dd0be1888b5ba800346eaa0123c1691a15050565b6040600081812080548490039055600160a060020a03808516808352929091208054840190553316606090815260809190915260a08290527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9080a1505056",
"devdoc": {
"methods": {
}
},
"userdoc": {
"methods": {
}
}
},
"address": "0x3a020580345e79e223580d8d6a50e063667f19b5"
}
Another is temp/migrations/Token.js
var TokenContract = web3.eth.contract([{"constant":false, "inputs":[{"name":"account", "type":"address"}, {"name":"amount", "type":"uint256"}], "name":"issue", "outputs":[], "payable":false, "type":"function"}, {"constant":false, "inputs":[{"name":"to", "type":"address"}, {"name":"amount", "type":"uint256"}], "name":"transfer", "outputs":[], "payable":false, "type":"function"}, {"constant":true, "inputs":[{"name":"account", "type":"address"}], "name":"getBalance", "outputs":[{"name":"", "type":"uint256"}], "payable":false, "type":"function"}, {"inputs":[], "type":"constructor"}, {"anonymous":false, "inputs":[{"indexed":false, "name":"account", "type":"address"}, {"indexed":false, "name":"amount", "type":"uint256"}], "name":"Issue", "type":"event"}, {"anonymous":false, "inputs":[{"indexed":false, "name":"from", "type":"address"}, {"indexed":false, "name":"to", "type":"address"}, {"indexed":false, "name":"amount", "type":"uint256"}], "name":"Transfer", "type":"event"}]);
var Token = TokenContract.at('0x80d29fb7f81d2ccd77c708b6135389c9c08653dc');
Deploy all contracts if no contract name provided.
Write your own javascript test
gtests/Token_test.js
loadScript('temp/migrations/Token.js');
var balance = Token.getBalance.call(web3.eth.accounts[0], { from: web3.eth.accounts[0] })
console.log("balance is: ", balance);
Token.issue.sendTransaction(web3.eth.accounts[0], 10000, { from: web3.eth.accounts[0] }, function(err, tx){
if(err){
console.log("issue error!");
} else {
console.log("issue success. tx: ", tx);
}
})
miner.start();admin.sleepBlocks(2);miner.stop();
balance = Token.getBalance.call(web3.eth.accounts[0], { from: web3.eth.accounts[0] })
console.log("balance is: ", balance);
Run gtest
$ teth gt token
***** Using geth at: geth
Testing contract Token...
balance is: 0
issue success. tx: 0x7fd24d1903345d4f70208c41fc3a1bd71be63f8dd7db7c654f2d3a7c176b4031
balance is: 10000
true
Done.
🍻
Frequently Asked Questions
Error: Account does not exist or account balance too low while teth migrate token
This is just low balance for your accoutns. Just mining for a while will be ok.
$ teth c
***** Using geth at: geth
Starting geth attach...
Welcome to the Geth JavaScript console!
instance: Geth/v1.4.11-stable-fed692f6/darwin/go1.7
coinbase: 0x3ae88fe370c39384fc16da2c9e768cf5d2495b48
at block: 3 (Sat, 17 Sep 2016 17:42:50 CST)
datadir: data
modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
> miner.start()
true
> web3.eth.getBalance(web3.eth.accounts[0])
506406250000000000000
>
TODO:
- Add chai for js test