Testing with Ethereum Tester

Ethereum Tester is a tool suite for testing Ethereum based applications.

This section provides a quick overview of testing with eth-tester. To learn more, you can view the documentation at the Github repo or join the Gitter channel.

Getting Started

Prior to testing, the Vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in conftest.py.

Note

Since the testing is done in the pytest framework, you can make use of pytest.ini, tox.ini and setup.cfg and you can use most IDEs’ pytest plugins.

conftest.py
  1import json
  2import logging
  3from contextlib import contextmanager
  4from functools import wraps
  5
  6import hypothesis
  7import pytest
  8import web3.exceptions
  9from eth_tester import EthereumTester, PyEVMBackend
 10from eth_tester.exceptions import TransactionFailed
 11from eth_utils import setup_DEBUG2_logging
 12from eth_utils.toolz import compose
 13from hexbytes import HexBytes
 14from web3 import Web3
 15from web3.contract import Contract
 16from web3.providers.eth_tester import EthereumTesterProvider
 17
 18import vyper.compiler.settings as compiler_settings
 19import vyper.evm.opcodes as evm
 20from tests.utils import working_directory
 21from vyper import compiler
 22from vyper.ast.grammar import parse_vyper_source
 23from vyper.codegen.ir_node import IRnode
 24from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle
 25from vyper.compiler.settings import (
 26    OptimizationLevel,
 27    Settings,
 28    get_global_settings,
 29    set_global_settings,
 30)
 31from vyper.evm.opcodes import EVM_VERSIONS, version_check
 32from vyper.exceptions import EvmVersionException
 33from vyper.ir import compile_ir, optimizer
 34from vyper.utils import ERC5202_PREFIX, keccak256
 35
 36# Import the base fixtures
 37pytest_plugins = ["tests.fixtures.memorymock"]
 38
 39############
 40# PATCHING #
 41############
 42
 43
 44# disable hypothesis deadline globally
 45hypothesis.settings.register_profile("ci", deadline=None)
 46hypothesis.settings.load_profile("ci")
 47
 48
 49def set_evm_verbose_logging():
 50    logger = logging.getLogger("eth.vm.computation.BaseComputation")
 51    setup_DEBUG2_logging()
 52    logger.setLevel("DEBUG2")
 53
 54
 55# Useful options to comment out whilst working:
 56# set_evm_verbose_logging()
 57#
 58# from vdb import vdb
 59# vdb.set_evm_opcode_debugger()
 60
 61
 62def pytest_addoption(parser):
 63    parser.addoption(
 64        "--optimize",
 65        choices=["codesize", "gas", "none"],
 66        default="gas",
 67        help="change optimization mode",
 68    )
 69    parser.addoption("--enable-compiler-debug-mode", action="store_true")
 70    parser.addoption("--experimental-codegen", action="store_true")
 71
 72    parser.addoption(
 73        "--evm-version",
 74        choices=list(EVM_VERSIONS.keys()),
 75        default="shanghai",
 76        help="set evm version",
 77    )
 78
 79
 80@pytest.fixture(scope="module")
 81def output_formats():
 82    output_formats = compiler.OUTPUT_FORMATS.copy()
 83    del output_formats["bb"]
 84    del output_formats["bb_runtime"]
 85    del output_formats["cfg"]
 86    del output_formats["cfg_runtime"]
 87    return output_formats
 88
 89
 90@pytest.fixture(scope="session")
 91def optimize(pytestconfig):
 92    flag = pytestconfig.getoption("optimize")
 93    return OptimizationLevel.from_string(flag)
 94
 95
 96@pytest.fixture(scope="session")
 97def debug(pytestconfig):
 98    debug = pytestconfig.getoption("enable_compiler_debug_mode")
 99    assert isinstance(debug, bool)
100    return debug
101
102
103@pytest.fixture(scope="session")
104def experimental_codegen(pytestconfig):
105    ret = pytestconfig.getoption("experimental_codegen")
106    assert isinstance(ret, bool)
107    return ret
108
109
110@pytest.fixture(scope="session")
111def evm_version(pytestconfig):
112    # note: we configure the evm version that we emit code for,
113    # but eth-tester is only configured with the latest mainnet
114    # version. luckily, evms are backwards compatible.
115    evm_version_str = pytestconfig.getoption("evm_version")
116    assert isinstance(evm_version_str, str)
117    return evm_version_str
118
119
120@pytest.fixture(scope="session", autouse=True)
121def global_settings(evm_version, experimental_codegen, optimize, debug):
122    evm.DEFAULT_EVM_VERSION = evm_version
123    compiler_settings.DEFAULT_ENABLE_DECIMALS = True
124    settings = Settings(
125        optimize=optimize,
126        evm_version=evm_version,
127        experimental_codegen=experimental_codegen,
128        debug=debug,
129    )
130    set_global_settings(settings)
131
132
133@pytest.fixture(autouse=True)
134def check_venom_xfail(request, experimental_codegen):
135    if not experimental_codegen:
136        return
137
138    marker = request.node.get_closest_marker("venom_xfail")
139    if marker is None:
140        return
141
142    # https://github.com/okken/pytest-runtime-xfail?tab=readme-ov-file#alternatives
143    request.node.add_marker(pytest.mark.xfail(strict=True, **marker.kwargs))
144
145
146@pytest.fixture
147def venom_xfail(request, experimental_codegen):
148    def _xfail(*args, **kwargs):
149        if not experimental_codegen:
150            return
151        request.node.add_marker(pytest.mark.xfail(*args, strict=True, **kwargs))
152
153    return _xfail
154
155
156@pytest.fixture
157def chdir_tmp_path(tmp_path):
158    # this is useful for when you want imports to have relpaths
159    with working_directory(tmp_path):
160        yield
161
162
163# CMC 2024-03-01 this doesn't need to be a fixture
164@pytest.fixture
165def keccak():
166    return keccak256
167
168
169@pytest.fixture
170def make_file(tmp_path):
171    # writes file_contents to file_name, creating it in the
172    # tmp_path directory. returns final path.
173    def fn(file_name, file_contents):
174        path = tmp_path / file_name
175        path.parent.mkdir(parents=True, exist_ok=True)
176        with path.open("w") as f:
177            f.write(file_contents)
178
179        return path
180
181    return fn
182
183
184# this can either be used for its side effects (to prepare a call
185# to get_contract), or the result can be provided directly to
186# compile_code / CompilerData.
187@pytest.fixture
188def make_input_bundle(tmp_path, make_file):
189    def fn(sources_dict):
190        for file_name, file_contents in sources_dict.items():
191            make_file(file_name, file_contents)
192        return FilesystemInputBundle([tmp_path])
193
194    return fn
195
196
197# for tests which just need an input bundle, doesn't matter what it is
198@pytest.fixture
199def dummy_input_bundle():
200    return InputBundle([])
201
202
203# TODO: remove me, this is just string.encode("utf-8").ljust()
204# only used in test_logging.py.
205@pytest.fixture
206def bytes_helper():
207    def bytes_helper(str, length):
208        return bytes(str, "utf-8") + bytearray(length - len(str))
209
210    return bytes_helper
211
212
213def _none_addr(datatype, data):
214    if datatype == "address" and int(data, base=16) == 0:
215        return (datatype, None)
216    else:
217        return (datatype, data)
218
219
220CONCISE_NORMALIZERS = (_none_addr,)
221
222
223@pytest.fixture(scope="module")
224def tester():
225    # set absurdly high gas limit so that london basefee never adjusts
226    # (note: 2**63 - 1 is max that evm allows)
227    custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 10**10})
228    custom_genesis["base_fee_per_gas"] = 0
229    backend = PyEVMBackend(genesis_parameters=custom_genesis)
230    return EthereumTester(backend=backend)
231
232
233def zero_gas_price_strategy(web3, transaction_params=None):
234    return 0  # zero gas price makes testing simpler.
235
236
237@pytest.fixture(scope="module")
238def w3(tester):
239    w3 = Web3(EthereumTesterProvider(tester))
240    w3.eth.set_gas_price_strategy(zero_gas_price_strategy)
241    return w3
242
243
244def get_compiler_gas_estimate(code, func):
245    sigs = compiler.phases.CompilerData(code).function_signatures
246    if func:
247        return compiler.utils.build_gas_estimates(sigs)[func] + 22000
248    else:
249        return sum(compiler.utils.build_gas_estimates(sigs).values()) + 22000
250
251
252def check_gas_on_chain(w3, tester, code, func=None, res=None):
253    gas_estimate = get_compiler_gas_estimate(code, func)
254    gas_actual = tester.get_block_by_number("latest")["gas_used"]
255    # Computed upper bound on the gas consumption should
256    # be greater than or equal to the amount of gas used
257    if gas_estimate < gas_actual:
258        raise Exception(f"Gas upper bound fail: bound {gas_estimate} actual {gas_actual}")
259
260    print(f"Function name: {func} - Gas estimate {gas_estimate}, Actual: {gas_actual}")
261
262
263def gas_estimation_decorator(w3, tester, fn, source_code, func):
264    def decorator(*args, **kwargs):
265        @wraps(fn)
266        def decorated_function(*args, **kwargs):
267            result = fn(*args, **kwargs)
268            if "transact" in kwargs:
269                check_gas_on_chain(w3, tester, source_code, func, res=result)
270            return result
271
272        return decorated_function(*args, **kwargs)
273
274    return decorator
275
276
277def set_decorator_to_contract_function(w3, tester, contract, source_code, func):
278    func_definition = getattr(contract, func)
279    func_with_decorator = gas_estimation_decorator(w3, tester, func_definition, source_code, func)
280    setattr(contract, func, func_with_decorator)
281
282
283class VyperMethod:
284    ALLOWED_MODIFIERS = {"call", "estimateGas", "transact", "buildTransaction"}
285
286    def __init__(self, function, normalizers=None):
287        self._function = function
288        self._function._return_data_normalizers = normalizers
289
290    def __call__(self, *args, **kwargs):
291        return self.__prepared_function(*args, **kwargs)
292
293    def __prepared_function(self, *args, **kwargs):
294        if not kwargs:
295            modifier, modifier_dict = "call", {}
296            fn_abi = [
297                x
298                for x in self._function.contract_abi
299                if x.get("name") == self._function.function_identifier
300            ].pop()
301            # To make tests faster just supply some high gas value.
302            modifier_dict.update({"gas": fn_abi.get("gas", 0) + 500000})
303        elif len(kwargs) == 1:
304            modifier, modifier_dict = kwargs.popitem()
305            if modifier not in self.ALLOWED_MODIFIERS:
306                raise TypeError(f"The only allowed keyword arguments are: {self.ALLOWED_MODIFIERS}")
307        else:
308            raise TypeError(f"Use up to one keyword argument, one of: {self.ALLOWED_MODIFIERS}")
309        return getattr(self._function(*args), modifier)(modifier_dict)
310
311
312class VyperContract:
313    """
314    An alternative Contract Factory which invokes all methods as `call()`,
315    unless you add a keyword argument. The keyword argument assigns the prep method.
316    This call
317    > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...})
318    is equivalent to this call in the classic contract:
319    > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...})
320    """
321
322    def __init__(self, classic_contract, method_class=VyperMethod):
323        classic_contract._return_data_normalizers += CONCISE_NORMALIZERS
324        self._classic_contract = classic_contract
325        self.address = self._classic_contract.address
326        protected_fn_names = [fn for fn in dir(self) if not fn.endswith("__")]
327
328        try:
329            fn_names = [fn["name"] for fn in self._classic_contract.functions._functions]
330        except web3.exceptions.NoABIFunctionsFound:
331            fn_names = []
332
333        for fn_name in fn_names:
334            # Override namespace collisions
335            if fn_name in protected_fn_names:
336                raise AttributeError(f"{fn_name} is protected!")
337            else:
338                _classic_method = getattr(self._classic_contract.functions, fn_name)
339                _concise_method = method_class(
340                    _classic_method, self._classic_contract._return_data_normalizers
341                )
342            setattr(self, fn_name, _concise_method)
343
344    @classmethod
345    def factory(cls, *args, **kwargs):
346        return compose(cls, Contract.factory(*args, **kwargs))
347
348
349@pytest.fixture
350def get_contract_from_ir(w3, optimize):
351    def ir_compiler(ir, *args, **kwargs):
352        ir = IRnode.from_list(ir)
353        if optimize != OptimizationLevel.NONE:
354            ir = optimizer.optimize(ir)
355
356        bytecode, _ = compile_ir.assembly_to_evm(
357            compile_ir.compile_to_assembly(ir, optimize=optimize)
358        )
359
360        abi = kwargs.get("abi") or []
361        c = w3.eth.contract(abi=abi, bytecode=bytecode)
362        deploy_transaction = c.constructor()
363        tx_hash = deploy_transaction.transact()
364        address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
365        contract = w3.eth.contract(
366            address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract
367        )
368        return contract
369
370    return ir_compiler
371
372
373def _get_contract(
374    w3,
375    source_code,
376    optimize,
377    experimental_codegen,
378    output_formats,
379    *args,
380    override_opt_level=None,
381    input_bundle=None,
382    **kwargs,
383):
384    settings = get_global_settings()
385    settings.optimize = override_opt_level or optimize
386    settings.experimental_codegen = experimental_codegen
387    out = compiler.compile_code(
388        source_code,
389        # test that all output formats can get generated
390        output_formats=output_formats,
391        settings=settings,
392        input_bundle=input_bundle,
393        show_gas_estimates=True,  # Enable gas estimates for testing
394    )
395    parse_vyper_source(source_code)  # Test grammar.
396    json.dumps(out["metadata"])  # test metadata is json serializable
397    abi = out["abi"]
398    bytecode = out["bytecode"]
399    value = kwargs.pop("value_in_eth", 0) * 10**18  # Handle deploying with an eth value.
400    c = w3.eth.contract(abi=abi, bytecode=bytecode)
401    deploy_transaction = c.constructor(*args)
402    tx_info = {"from": w3.eth.accounts[0], "value": value, "gasPrice": 0}
403    tx_info.update(kwargs)
404    tx_hash = deploy_transaction.transact(tx_info)
405    address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
406    return w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract)
407
408
409@pytest.fixture(scope="module")
410def get_contract(w3, optimize, experimental_codegen, output_formats):
411    def fn(source_code, *args, **kwargs):
412        return _get_contract(
413            w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
414        )
415
416    return fn
417
418
419@pytest.fixture
420def get_contract_with_gas_estimation(tester, w3, optimize, experimental_codegen, output_formats):
421    def get_contract_with_gas_estimation(source_code, *args, **kwargs):
422        contract = _get_contract(
423            w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
424        )
425        for abi_ in contract._classic_contract.functions.abi:
426            if abi_["type"] == "function":
427                set_decorator_to_contract_function(w3, tester, contract, source_code, abi_["name"])
428        return contract
429
430    return get_contract_with_gas_estimation
431
432
433@pytest.fixture
434def get_contract_with_gas_estimation_for_constants(
435    w3, optimize, experimental_codegen, output_formats
436):
437    def get_contract_with_gas_estimation_for_constants(source_code, *args, **kwargs):
438        return _get_contract(
439            w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
440        )
441
442    return get_contract_with_gas_estimation_for_constants
443
444
445@pytest.fixture(scope="module")
446def get_contract_module(optimize, experimental_codegen, output_formats):
447    """
448    This fixture is used for Hypothesis tests to ensure that
449    the same contract is called over multiple runs of the test.
450    """
451    custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 4500000})
452    custom_genesis["base_fee_per_gas"] = 0
453    backend = PyEVMBackend(genesis_parameters=custom_genesis)
454    tester = EthereumTester(backend=backend)
455    w3 = Web3(EthereumTesterProvider(tester))
456    w3.eth.set_gas_price_strategy(zero_gas_price_strategy)
457
458    def get_contract_module(source_code, *args, **kwargs):
459        return _get_contract(
460            w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
461        )
462
463    return get_contract_module
464
465
466def _deploy_blueprint_for(
467    w3,
468    source_code,
469    optimize,
470    experimental_codegen,
471    output_formats,
472    initcode_prefix=ERC5202_PREFIX,
473    **kwargs,
474):
475    settings = Settings()
476    settings.optimize = optimize
477    settings.experimental_codegen = experimental_codegen
478    out = compiler.compile_code(
479        source_code,
480        output_formats=output_formats,
481        settings=settings,
482        show_gas_estimates=True,  # Enable gas estimates for testing
483    )
484    parse_vyper_source(source_code)  # Test grammar.
485    abi = out["abi"]
486    bytecode = HexBytes(initcode_prefix) + HexBytes(out["bytecode"])
487    bytecode_len = len(bytecode)
488    bytecode_len_hex = hex(bytecode_len)[2:].rjust(4, "0")
489    # prepend a quick deploy preamble
490    deploy_preamble = HexBytes("61" + bytecode_len_hex + "3d81600a3d39f3")
491    deploy_bytecode = HexBytes(deploy_preamble) + bytecode
492
493    deployer_abi = []  # just a constructor
494    c = w3.eth.contract(abi=deployer_abi, bytecode=deploy_bytecode)
495    deploy_transaction = c.constructor()
496    tx_info = {"from": w3.eth.accounts[0], "value": 0, "gasPrice": 0}
497
498    tx_hash = deploy_transaction.transact(tx_info)
499    address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
500
501    # sanity check
502    assert w3.eth.get_code(address) == bytecode, (w3.eth.get_code(address), bytecode)
503
504    def factory(address):
505        return w3.eth.contract(
506            address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract
507        )
508
509    return w3.eth.contract(address, bytecode=deploy_bytecode), factory
510
511
512@pytest.fixture(scope="module")
513def deploy_blueprint_for(w3, optimize, experimental_codegen, output_formats):
514    def deploy_blueprint_for(source_code, *args, **kwargs):
515        return _deploy_blueprint_for(
516            w3, source_code, optimize, experimental_codegen, output_formats, *args, **kwargs
517        )
518
519    return deploy_blueprint_for
520
521
522# TODO: this should not be a fixture.
523# remove me and replace all uses with `with pytest.raises`.
524@pytest.fixture
525def assert_compile_failed():
526    def assert_compile_failed(function_to_test, exception=Exception):
527        with pytest.raises(exception):
528            function_to_test()
529
530    return assert_compile_failed
531
532
533@pytest.fixture
534def create2_address_of(keccak):
535    def _f(_addr, _salt, _initcode):
536        prefix = HexBytes("0xff")
537        addr = HexBytes(_addr)
538        salt = HexBytes(_salt)
539        initcode = HexBytes(_initcode)
540        return keccak(prefix + addr + salt + keccak(initcode))[12:]
541
542    return _f
543
544
545@pytest.fixture
546def side_effects_contract(get_contract):
547    def generate(ret_type):
548        """
549        Generates a Vyper contract with an external `foo()` function, which
550        returns the specified return value of the specified return type, for
551        testing side effects using the `assert_side_effects_invoked` fixture.
552        """
553        code = f"""
554counter: public(uint256)
555
556@external
557def foo(s: {ret_type}) -> {ret_type}:
558    self.counter += 1
559    return s
560    """
561        contract = get_contract(code)
562        return contract
563
564    return generate
565
566
567@pytest.fixture
568def assert_side_effects_invoked():
569    def assert_side_effects_invoked(side_effects_contract, side_effects_trigger, n=1):
570        start_value = side_effects_contract.counter()
571
572        side_effects_trigger()
573
574        end_value = side_effects_contract.counter()
575        assert end_value == start_value + n
576
577    return assert_side_effects_invoked
578
579
580@pytest.fixture
581def get_logs(w3):
582    def get_logs(tx_hash, c, event_name):
583        tx_receipt = w3.eth.get_transaction_receipt(tx_hash)
584        return c._classic_contract.events[event_name]().process_receipt(tx_receipt)
585
586    return get_logs
587
588
589@pytest.fixture(scope="module")
590def tx_failed(tester):
591    @contextmanager
592    def fn(exception=TransactionFailed, exc_text=None):
593        snapshot_id = tester.take_snapshot()
594        with pytest.raises(exception) as excinfo:
595            yield excinfo
596        tester.revert_to_snapshot(snapshot_id)
597        if exc_text:
598            # TODO test equality
599            assert exc_text in str(excinfo.value), (exc_text, excinfo.value)
600
601    return fn
602
603
604def pytest_runtest_call(item):
605    marker = item.get_closest_marker("requires_evm_version")
606    if marker:
607        assert len(marker.args) == 1
608        version = marker.args[0]
609        if not version_check(begin=version):
610            item.add_marker(
611                pytest.mark.xfail(reason="Wrong EVM version", raises=EvmVersionException)
612            )

The final two fixtures are optional and will be discussed later. The rest of this chapter assumes that you have this code set up in your conftest.py file.

Alternatively, you can import the fixtures to conftest.py or use pytest plugins.

Writing a Basic Test

Assume the following simple contract storage.vy. It has a single integer variable and a function to set that value.

storage.vy
 1#pragma version >0.3.10
 2
 3storedData: public(int128)
 4
 5@deploy
 6def __init__(_x: int128):
 7  self.storedData = _x
 8
 9@external
10def set(_x: int128):
11  self.storedData = _x

We create a test file test_storage.py where we write our tests in pytest style.

test_storage.py
 1import pytest
 2
 3INITIAL_VALUE = 4
 4
 5
 6@pytest.fixture
 7def storage_contract(w3, get_contract):
 8    with open("examples/storage/storage.vy") as f:
 9        contract_code = f.read()
10        # Pass constructor variables directly to the contract
11        contract = get_contract(contract_code, INITIAL_VALUE)
12    return contract
13
14
15def test_initial_state(storage_contract):
16    # Check if the constructor of the contract is set up properly
17    assert storage_contract.storedData() == INITIAL_VALUE
18
19
20def test_set(w3, storage_contract):
21    k0 = w3.eth.accounts[0]
22
23    # Let k0 try to set the value to 10
24    storage_contract.set(10, transact={"from": k0})
25    assert storage_contract.storedData() == 10  # Directly access storedData
26
27    # Let k0 try to set the value to -5
28    storage_contract.set(-5, transact={"from": k0})
29    assert storage_contract.storedData() == -5

First we create a fixture for the contract which will compile our contract and set up a Web3 contract object. We then use this fixture for our test functions to interact with the contract.

Note

To run the tests, call pytest or python -m pytest from your project directory.

Events and Failed Transactions

To test events and failed transactions we expand our simple storage contract to include an event and two conditions for a failed transaction: advanced_storage.vy

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

Next, we take a look at the two fixtures that will allow us to read the event logs and to check for failed transactions.

conftest.py
@pytest.fixture(scope="module")
def tx_failed(tester):
    @contextmanager
    def fn(exception=TransactionFailed, exc_text=None):
        snapshot_id = tester.take_snapshot()
        with pytest.raises(exception) as excinfo:
            yield excinfo
        tester.revert_to_snapshot(snapshot_id)
        if exc_text:
            # TODO test equality
            assert exc_text in str(excinfo.value), (exc_text, excinfo.value)

    return fn

The fixture to assert failed transactions defaults to check for a TransactionFailed exception, but can be used to check for different exceptions too, as shown below. Also note that the chain gets reverted to the state before the failed transaction.

conftest.py
@pytest.fixture
def get_logs(w3):
    def get_logs(tx_hash, c, event_name):
        tx_receipt = w3.eth.get_transaction_receipt(tx_hash)
        return c._classic_contract.events[event_name]().process_receipt(tx_receipt)

    return get_logs

This fixture will return a tuple with all the logs for a certain event and transaction. The length of the tuple equals the number of events (of the specified type) logged and should be checked first.

Finally, we create a new file test_advanced_storage.py where we use the new fixtures to test failed transactions and events.

test_advanced_storage.py
 1import pytest
 2from web3.exceptions import ValidationError
 3
 4INITIAL_VALUE = 4
 5
 6
 7@pytest.fixture
 8def adv_storage_contract(w3, get_contract):
 9    with open("examples/storage/advanced_storage.vy") as f:
10        contract_code = f.read()
11        # Pass constructor variables directly to the contract
12        contract = get_contract(contract_code, INITIAL_VALUE)
13    return contract
14
15
16def test_initial_state(adv_storage_contract):
17    # Check if the constructor of the contract is set up properly
18    assert adv_storage_contract.storedData() == INITIAL_VALUE
19
20
21def test_failed_transactions(w3, adv_storage_contract, tx_failed):
22    k1 = w3.eth.accounts[1]
23
24    # Try to set the storage to a negative amount
25    with tx_failed():
26        adv_storage_contract.set(-10, transact={"from": k1})
27
28    # Lock the contract by storing more than 100. Then try to change the value
29    adv_storage_contract.set(150, transact={"from": k1})
30    with tx_failed():
31        adv_storage_contract.set(10, transact={"from": k1})
32
33    # Reset the contract and try to change the value
34    adv_storage_contract.reset(transact={"from": k1})
35    adv_storage_contract.set(10, transact={"from": k1})
36    assert adv_storage_contract.storedData() == 10
37
38    # Assert a different exception (ValidationError for non-matching argument type)
39    with tx_failed(ValidationError):
40        adv_storage_contract.set("foo", transact={"from": k1})
41
42    # Assert a different exception that contains specific text
43    with tx_failed(ValidationError, "invocation failed due to improper number of arguments"):
44        adv_storage_contract.set(1, 2, transact={"from": k1})
45
46
47def test_events(w3, adv_storage_contract, get_logs):
48    k1, k2 = w3.eth.accounts[:2]
49
50    tx1 = adv_storage_contract.set(10, transact={"from": k1})
51    tx2 = adv_storage_contract.set(20, transact={"from": k2})
52    tx3 = adv_storage_contract.reset(transact={"from": k1})
53
54    # Save DataChange logs from all three transactions
55    logs1 = get_logs(tx1, adv_storage_contract, "DataChange")
56    logs2 = get_logs(tx2, adv_storage_contract, "DataChange")
57    logs3 = get_logs(tx3, adv_storage_contract, "DataChange")
58
59    # Check log contents
60    assert len(logs1) == 1
61    assert logs1[0].args.value == 10
62
63    assert len(logs2) == 1
64    assert logs2[0].args.setter == k2
65
66    assert not logs3  # tx3 does not generate a log