Testing with Brownie¶
Brownie is a Python-based development and testing framework for smart contracts. It includes a pytest plugin with fixtures that simplify testing your contract.
This section provides a quick overview of testing with Brownie. To learn more, you can view the Brownie documentation on writing unit tests or join the Ethereum Python Dev Discord #brownie
channel.
Getting Started¶
In order to use Brownie for testing you must first initialize a new project. Create a new directory for the project, and from within that directory type:
$ brownie init
This will create an empty project structure within the directory. Store your contract sources within the project’s contracts/
directory and your tests within tests/
.
Writing a Basic Test¶
Assume the following simple contract Storage.vy
. It has a single integer variable and a function to set that value.
1 2 3 4 5 6 7 8 9 | storedData: public(int128)
@external
def __init__(_x: int128):
self.storedData = _x
@external
def set(_x: int128):
self.storedData = _x
|
We create a test file tests/test_storage.py
where we write our tests in pytest style.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pytest
INITIAL_VALUE = 4
@pytest.fixture
def storage_contract(Storage, accounts):
# deploy the contract with the initial value as a constructor argument
yield Storage.deploy(INITIAL_VALUE, {'from': accounts[0]})
def test_initial_state(storage_contract):
# Check if the constructor of the contract is set up properly
assert storage_contract.storedData() == INITIAL_VALUE
def test_set(storage_contract, accounts):
# set the value to 10
storage_contract.set(10, {'from': accounts[0]})
assert storage_contract.storedData() == 10 # Directly access storedData
# set the value to -5
storage_contract.set(-5, {'from': accounts[0]})
assert storage_contract.storedData() == -5
|
In this example we are using two fixtures which are provided by Brownie:
accounts
provides access to theAccounts
container, containing all of your local accountsStorage
is a dynamically named fixture that provides access to aContractContainer
object, used to deploy your contract
Note
To run the tests, use the brownie test
command from the root directory of your project.
Testing Events¶
For the remaining examples, we expand our simple storage contract to include an event and two conditions for a failed transaction: AdvancedStorage.vy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | event DataChange:
setter: indexed(address)
value: int128
storedData: public(int128)
@external
def __init__(_x: int128):
self.storedData = _x
@external
def set(_x: int128):
assert _x >= 0, "No negative values"
assert self.storedData < 100, "Storage is locked when 100 or more is stored"
self.storedData = _x
log DataChange(msg.sender, _x)
@external
def reset():
self.storedData = 0
|
To test events, we examine the TransactionReceipt
object which is returned after each successful transaction. It contains an events
member with information about events that fired.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import brownie
INITIAL_VALUE = 4
@pytest.fixture
def adv_storage_contract(AdvancedStorage, accounts):
yield AdvancedStorage.deploy(INITIAL_VALUE, {'from': accounts[0]})
def test_events(adv_storage_contract, accounts):
tx1 = adv_storage_contract.set(10, {'from': accounts[0])
tx2 = adv_storage_contract.set(20, {'from': accounts[1])
tx3 = adv_storage_contract.reset({'from': accounts[0])
# Check log contents
assert len(tx1.events) == 1
assert tx1.events[0]['value'] == 10
assert len(tx2.events) == 1
assert tx2.events[0]['setter'] == accounts[1]
assert not tx3.events # tx3 does not generate a log
|
Handling Reverted Transactions¶
Transactions that revert raise a VirtualMachineError
exception. To write assertions around this you can use brownie.reverts
as a context manager. It functions very similarly to pytest.raises
.
brownie.reverts
optionally accepts a string as an argument. If given, the error string returned by the transaction must match it in order for the test to pass.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import brownie
INITIAL_VALUE = 4
@pytest.fixture
def adv_storage_contract(AdvancedStorage, accounts):
yield AdvancedStorage.deploy(INITIAL_VALUE, {'from': accounts[0]})
def test_failed_transactions(adv_storage_contract, accounts):
# Try to set the storage to a negative amount
with brownie.reverts("No negative values"):
adv_storage_contract.set(-10, {"from": accounts[1]})
# Lock the contract by storing more than 100. Then try to change the value
adv_storage_contract.set(150, {"from": accounts[1]})
with brownie.reverts("Storage is locked when 100 or more is stored"):
adv_storage_contract.set(10, {"from": accounts[1]})
# Reset the contract and try to change the value
adv_storage_contract.reset({"from": accounts[1]})
adv_storage_contract.set(10, {"from": accounts[1]})
assert adv_storage_contract.storedData() == 10
|