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 Gitter 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 the Accounts container, containing all of your local accounts
  • Storage is a dynamically named fixture that provides access to a ContractContainer 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