Part 4: Transaction Tests

Test a Contract Function

Edit the tests file and add another test:

$ nano contracts/test_donator.py

After the edit, the file should look as follows:

def test_defaultUsdRate(chain):
    donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
    defaultUsdRate = donator.call().defaultUsdRate()
    assert defaultUsdRate == 350


def test_donate(chain):
    donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
    donator.transact({'value':500}).donate(37)
    donator.transact({'value':650}).donate(38)
    donationsCount = donator.call().donationsCount()
    donationsTotal = donator.call().donationsTotal()
    defaultUsdRate = donator.call().defaultUsdRate()

    assert donationsTotal == 1150
    assert donationsCount == 2
    assert defaultUsdRate == 380

You added another test, test_donations. The second test is similar to the first one:

[1] Get the chain: The test function accepts the chain argument, the auto-generated Python object that corresponds to a tester chain. Reminder: the tester chain is ephimeral, in memory, and reset on each test function.

[2] Get the contract: With the magic function get_or_deploy_contract Populus compiles the Donator contract, deploys it to the chain, creates a Web3 contract object, and returns it to the function as a Python object with Python methods. This object is stored in the donator variable.

[3] The “transact” function:

donator.transact({'value':500}).donate(37)

Reminder: we have two options to interact with a contract on the blockchain, transactions and calls. With Populus, you initiate a transaction with transact, and a call with call:

  • Transactions: Send a transaction, run the contract code, transfer funds, and change the state of the contract and it’s balance. This change will be permanent, and synced to the entire blockchain.
  • Call: Behaves exactly as a transaction, but once done, everything is revert and no state is changed. A call is kinda “dry-run”, and an efficient way to query the current state without expensive gas costs.

[4] Test transactions: The test commits two transactions, and send funds in both. In the first the value of the funds is 500, and in the second the value is 650. The value is provided as a transact argument, in a dictionary, where you can add more kwargs of an Ethereum transaction.

Note

Since these are transactions, they will change state, and in the case of the tester chain this state will persist until the test function quits.

[5] Providing arguments: The donate function in the contract accepts one argument

function donate(uint usd_rate) public payable nonZeroValue {...}

This argument is provided in the test as Python donate function:

donator.transact({'value':650}).donate(38).

Populus gives you a Python interface to a bytecode contract. Nice, no?

[6] Asserts: We expect the donationsTotal to be 500 + 650 = 1150, the donationsCount is 2, and the defaultUsdRate to match the last update, 380.

The test gets the varaibles with call, and should update instanrly because it’s a local tester chain. On a distributed blockchain it will take sometime until the transactions are mined and actually change the state.

Run the test:

$ py.test --disable-pytest-warnings

platform linux -- Python 3.5.2, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/mary/projects/donations, inifile:
plugins: populus-1.8.0, hypothesis-3.14.0
collected 2 items

tests/test_donator.py ..

===================== 2 passed, 10 warnings in 0.58 seconds =============

Voila. The two tests pass.

Test Calculations

The next one will test the ETH/USD calculations:

$ nano tests/test_donator.py

Add the following test to the bottom of the file:

def test_usd_calculation(chain):

    ONE_ETH_IN_WEI = 10**18  # 1 ETH == 1,000,000,000,000,000,000 Wei

    donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
    donator.transact({'value':ONE_ETH_IN_WEI}).donate(4)
    donator.transact({'value':(2 * ONE_ETH_IN_WEI)}).donate(5)
    donationsUsd = donator.call().donationsUsd()

    # donated 1 ETH in  $4 per ETH = $4
    # donated 2 ETH in $5 per ETH = 2 * $5 = $10
    # total $ value donated = $4 + $10 = $14
    assert donationsUsd == 14

The test sends donations worth of 3 Ether. Reminder: by default, all contract functions and contract interactions are handled in Wei.

In 1 Ether we have 10^18 Wei (see the Ether units denominations)

The test runs two transactions: note the transact function, which will change the contract state and balance on the blockchain. We use the tester chain, so the state is reset on each test run.

First transaction

donator.transact({'value':ONE_ETH_IN_WEI}).donate(4)

Donate Wei worth of 1 Ether, where the effective ETH/USD rate is $4. That is, $4 per Ether, and a total USD value of $4

Second transaction

donator.transact({'value':(2 * ONE_ETH_IN_WEI)}).donate(5)

Donate Wei worth of 2 Ether, where the effective ETH/USD rate is $5 (no markets sepculations on the tutorial) It’s $5 per Ether, and total USD value of 2 * $5 = $10

Hence we excpect the total USD value of these two donations to be $4 + $10 = $14

donationsUsd = donator.call().donationsUsd()
assert donationsUsd == 14

OK, that wan’t too complicated. Run the test:

$ py.test --disable-pytest-warnings

And the py.test results:

platform linux -- Python 3.5.2, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/mary/projects/donations, inifile:
plugins: populus-1.8.0, hypothesis-3.14.0
collected 3 items

tests/test_donator.py ..F

================================ FAILURES =======================================================
__________________________ test_usd_calculation _________________________________________________

chain = <populus.chain.tester.TesterChain object at 0x7f2736d1c630>

    def test_usd_calculation(chain):

        ONE_ETH_IN_WEI = 10**18  # 1 ETH == 1,000,000,000,000,000,000 Wei

        donator, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator')
        donator.transact({'value':ONE_ETH_IN_WEI}).donate(4)
        donator.transact({'value':(2 * ONE_ETH_IN_WEI)}).donate(5)
        donationsUsd = donator.call().donationsUsd()

        # donated 1 ETH at $4 per ETH = $4
        # donated 2 ETH at $5 per ETH = 2 * $5 = $10
        # total $ value donated = $4 + $10 = $14
>       assert donationsUsd == 14
E       assert 14000000000000000000 == 14

tests/test_donator.py:32: AssertionError
======================================= 1 failed, 2 passed, 15 warnings in 0.95 seconds =========

Ooops. Something went wrong. But this is what tests are all about.

Py.test tells us that the assert failed. Instead of 14, the donationsUsd is 14000000000000000000. And you know the saying: a billion here, a billion there, and pretty soon you’re talking about real money.

Where is the bug? you maybe guessed it already, but let’s take a look at the contract’s donate function:

function donate(uint usd_rate) public payable nonZeroValue {
    donationsTotal += msg.value;
    donationsCount += 1;
    defaultUsdRate = usd_rate;
    uint inUsd = msg.value * usd_rate;
    donationsUsd += inUsd;
    }

Now it’s clear:

uint inUsd = msg.value * usd_rate;

This line multiplies msg.value, which is in Wei, by usd_rate, which is the exchange rate per Ether.

Reminder: as of 0.4.17 Solidity does not have a workable decimal point calculation, and you have to handle fixed-point with integers. For the sake of simplicity, we will stay with ints.

Edit the contract:

$ nano contracts/Donator.sol

We could fix the line into:

uint inUsd = msg.value * usd_rate / 10**18;

But Solidity can do the math for you, and Ether units are reserved words. So fix to:

uint inUsd = msg.value * usd_rate / 1 ether;

Run the tests again:

$ py.test --disable-pytest-warnings

==================================== test session starts ===================
platform linux -- Python 3.5.2, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/mary/projects/donations, inifile:
plugins: populus-1.8.0, hypothesis-3.14.0
collected 3 items

tests/test_donator.py ...

============================== 3 passed, 15 warnings in 0.93 seconds =======

Easy.

Warning

Note that if this contract was running on mainent, you could not fix it, and probably had to deploy a new one and loose the current contract and the money paid for it. This is why testing beforehand is so important with smart contracts.

Interim Summary

  • Three tests pass
  • Transactions tests pass
  • Exchange rate calculations pass
  • You fixed a bug in the contract source code.

The contract seems Ok, but to be on the safe side, we will run next a few tests for the edge cases.