diff --git a/docs/examples.rst b/docs/examples.rst index 6ab75b6d6e..8c933c6e1f 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -449,3 +449,25 @@ When someone has an allowance they can transfer those tokens using the .. _ERC20: https://github.com/ethereum/EIPs/blob/7f4f0377730f5fc266824084188cc17cf246932e/EIPS/eip-20.md + + +Contract Unit Tests in Python +----------------------------- + +Here is an example of how one can use the `pytest`_ framework in python, Web3.py, +eth-tester, and PyEVM to perform unit tests entirely in python without any +additional need for a full featured ethereum node/client. To install needed +dependencies you can use the pinned extra for eth_tester in web3 and pytest: + +.. _pytest: https://docs.pytest.org/en/latest/ + +.. code-block:: bash + + $ pip install web3[tester] pytest + +Once you have an environment set up for testing, you can then write your tests +like so: + +.. include:: ../tests/core/contracts/test_contract_example.py + :code: python + :start-line: 1 diff --git a/tests/core/contracts/test_contract_example.py b/tests/core/contracts/test_contract_example.py new file mode 100644 index 0000000000..43427e1561 --- /dev/null +++ b/tests/core/contracts/test_contract_example.py @@ -0,0 +1,103 @@ +# This file is used by the documentation as an example of how to write unit tests with web3.py +import pytest + +from web3 import ( + EthereumTesterProvider, + Web3, +) + + +@pytest.fixture +def tester_provider(): + return EthereumTesterProvider() + + +@pytest.fixture +def eth_tester(tester_provider): + return tester_provider.ethereum_tester + + +@pytest.fixture +def w3(tester_provider): + return Web3(tester_provider) + + +@pytest.fixture +def foo_contract(eth_tester, w3): + # For simplicity of this example we statically define the + # contract code here. You might read your contracts from a + # file, or something else to test with in your own code + # + # pragma solidity^0.5.3; + # + # contract Foo { + # + # string public bar; + # event barred(string _bar); + # + # constructor() public { + # bar = "hello world"; + # } + # + # function setBar(string memory _bar) public { + # bar = _bar; + # emit barred(_bar); + # } + # + # } + + deploy_address = eth_tester.get_accounts()[0] + + abi = """[{"anonymous":false,"inputs":[{"indexed":false,"name":"_bar","type":"string"}],"name":"barred","type":"event"},{"constant":false,"inputs":[{"name":"_bar","type":"string"}],"name":"setBar","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"bar","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]""" # noqa: E501 + # This bytecode is the output of compiling with + # solc version:0.5.3+commit.10d17f24.Emscripten.clang + bytecode = """608060405234801561001057600080fd5b506040805190810160405280600b81526020017f68656c6c6f20776f726c640000000000000000000000000000000000000000008152506000908051906020019061005c929190610062565b50610107565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100a357805160ff19168380011785556100d1565b828001600101855582156100d1579182015b828111156100d05782518255916020019190600101906100b5565b5b5090506100de91906100e2565b5090565b61010491905b808211156101005760008160009055506001016100e8565b5090565b90565b6103bb806101166000396000f3fe608060405234801561001057600080fd5b5060043610610053576000357c01000000000000000000000000000000000000000000000000000000009004806397bc14aa14610058578063febb0f7e14610113575b600080fd5b6101116004803603602081101561006e57600080fd5b810190808035906020019064010000000081111561008b57600080fd5b82018360208201111561009d57600080fd5b803590602001918460018302840111640100000000831117156100bf57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050509192919290505050610196565b005b61011b61024c565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561015b578082015181840152602081019050610140565b50505050905090810190601f1680156101885780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b80600090805190602001906101ac9291906102ea565b507f5f71ad82e16f082de5ff496b140e2fbc8621eeb37b36d59b185c3f1364bbd529816040518080602001828103825283818151815260200191508051906020019080838360005b8381101561020f5780820151818401526020810190506101f4565b50505050905090810190601f16801561023c5780820380516001836020036101000a031916815260200191505b509250505060405180910390a150565b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102e25780601f106102b7576101008083540402835291602001916102e2565b820191906000526020600020905b8154815290600101906020018083116102c557829003601f168201915b505050505081565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061032b57805160ff1916838001178555610359565b82800160010185558215610359579182015b8281111561035857825182559160200191906001019061033d565b5b509050610366919061036a565b5090565b61038c91905b80821115610388576000816000905550600101610370565b5090565b9056fea165627a7a72305820ae6ca683d45ee8a71bba45caee29e4815147cd308f772c853a20dfe08214dbb50029""" # noqa: E501 + + # Create our contract class. + FooContract = w3.eth.contract(abi=abi, bytecode=bytecode) + # issue a transaction to deploy the contract. + tx_hash = FooContract.constructor().transact({ + 'from': deploy_address, + }) + # wait for the transaction to be mined + tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash, 180) + # instantiate and return an instance of our contract. + return FooContract(tx_receipt.contractAddress) + + +def test_initial_greeting(foo_contract): + hw = foo_contract.caller.bar() + assert hw == "hello world" + + +def test_can_update_greeting(w3, foo_contract): + # send transaction that updates the greeting + tx_hash = foo_contract.functions.setBar( + "testing contracts is easy", + ).transact({ + 'from': w3.eth.accounts[1], + }) + w3.eth.waitForTransactionReceipt(tx_hash, 180) + + # verify that the contract is now using the updated greeting + hw = foo_contract.caller.bar() + assert hw == "testing contracts is easy" + + +def test_updating_greeting_emits_event(w3, foo_contract): + # send transaction that updates the greeting + tx_hash = foo_contract.functions.setBar( + "testing contracts is easy", + ).transact({ + 'from': w3.eth.accounts[1], + }) + receipt = w3.eth.waitForTransactionReceipt(tx_hash, 180) + + # get all of the `barred` logs for the contract + logs = foo_contract.events.barred.getLogs() + assert len(logs) == 1 + + # verify that the log's data matches the expected value + event = logs[0] + assert event.blockHash == receipt.blockHash + assert event.args._bar == "testing contracts is easy"