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.
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.
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.
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
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.
@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.
@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.
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