使用 Brownie 进行测试

Brownie 是一个基于 Python 的智能合约开发和测试框架。它包含一个 pytest 插件,其中包含简化测试合约的固定装置。

本节概述了使用 Brownie 进行测试。要了解更多信息,您可以查看 Brownie 文档,了解有关 编写单元测试 的信息,或加入 Ethereum Python Dev Discord #brownie 频道。

入门

为了使用 Brownie 进行测试,您必须首先 初始化一个新项目。为项目创建一个新目录,并在该目录中键入以下命令:

$ brownie init

这将在目录中创建一个空的 项目结构。将您的合约源代码存储在项目的 contracts/ 目录中,并将您的测试存储在 tests/ 中。

编写基本测试

假设以下简单的合约 Storage.vy。它具有一个整数变量和一个用于设置该值的函数。

1storedData: public(int128)
2
3@external
4def __init__(_x: int128):
5  self.storedData = _x
6
7@external
8def set(_x: int128):
9  self.storedData = _x

我们创建了一个测试文件 tests/test_storage.py,我们在其中以 pytest 风格编写测试。

 1import pytest
 2
 3INITIAL_VALUE = 4
 4
 5
 6@pytest.fixture
 7def storage_contract(Storage, accounts):
 8    # deploy the contract with the initial value as a constructor argument
 9    yield Storage.deploy(INITIAL_VALUE, {'from': accounts[0]})
10
11
12def test_initial_state(storage_contract):
13    # Check if the constructor of the contract is set up properly
14    assert storage_contract.storedData() == INITIAL_VALUE
15
16
17def test_set(storage_contract, accounts):
18    # set the value to 10
19    storage_contract.set(10, {'from': accounts[0]})
20    assert storage_contract.storedData() == 10  # Directly access storedData
21
22    # set the value to -5
23    storage_contract.set(-5, {'from': accounts[0]})
24    assert storage_contract.storedData() == -5

在本例中,我们使用了两个由 Brownie 提供的固定装置

  • accounts 提供对 Accounts 容器的访问,该容器包含您所有本地帐户

  • Storage 是一个动态命名的固定装置,它提供对 ContractContainer 对象的访问,用于部署您的合约

注意

要运行测试,请从项目的根目录使用 brownie test 命令。

测试事件

对于以下示例,我们将简单的存储合约扩展为包含一个事件和两个导致交易失败的条件:AdvancedStorage.vy

 1event DataChange:
 2    setter: indexed(address)
 3    value: int128
 4
 5storedData: public(int128)
 6
 7@external
 8def __init__(_x: int128):
 9  self.storedData = _x
10
11@external
12def set(_x: int128):
13  assert _x >= 0, "No negative values"
14  assert self.storedData < 100, "Storage is locked when 100 or more is stored"
15  self.storedData = _x
16  log DataChange(msg.sender, _x)
17
18@external
19def reset():
20  self.storedData = 0

为了测试事件,我们检查了 TransactionReceipt 对象,该对象在每次成功交易后返回。它包含一个 events 成员,其中包含有关触发事件的信息。

 1import brownie
 2
 3INITIAL_VALUE = 4
 4
 5
 6@pytest.fixture
 7def adv_storage_contract(AdvancedStorage, accounts):
 8    yield AdvancedStorage.deploy(INITIAL_VALUE, {'from': accounts[0]})
 9
10def test_events(adv_storage_contract, accounts):
11    tx1 = adv_storage_contract.set(10, {'from': accounts[0]})
12    tx2 = adv_storage_contract.set(20, {'from': accounts[1]})
13    tx3 = adv_storage_contract.reset({'from': accounts[0]})
14
15    # Check log contents
16    assert len(tx1.events) == 1
17    assert tx1.events[0]['value'] == 10
18
19    assert len(tx2.events) == 1
20    assert tx2.events[0]['setter'] == accounts[1]
21
22    assert not tx3.events   # tx3 does not generate a log

处理回滚的交易

回滚的交易会引发 VirtualMachineError 异常。要围绕此问题编写断言,您可以使用 brownie.reverts 作为上下文管理器。它的功能与 pytest.raises 非常相似。

brownie.reverts 可选地接受一个字符串作为参数。如果给出,则交易返回的错误字符串必须与之匹配,才能使测试通过。

 1import brownie
 2
 3INITIAL_VALUE = 4
 4
 5
 6@pytest.fixture
 7def adv_storage_contract(AdvancedStorage, accounts):
 8    yield AdvancedStorage.deploy(INITIAL_VALUE, {'from': accounts[0]})
 9
10
11def test_failed_transactions(adv_storage_contract, accounts):
12    # Try to set the storage to a negative amount
13    with brownie.reverts("No negative values"):
14        adv_storage_contract.set(-10, {"from": accounts[1]})
15
16    # Lock the contract by storing more than 100. Then try to change the value
17
18    adv_storage_contract.set(150, {"from": accounts[1]})
19    with brownie.reverts("Storage is locked when 100 or more is stored"):
20        adv_storage_contract.set(10, {"from": accounts[1]})
21
22    # Reset the contract and try to change the value
23    adv_storage_contract.reset({"from": accounts[1]})
24    adv_storage_contract.set(10, {"from": accounts[1]})
25    assert adv_storage_contract.storedData() == 10