Skip to content

Commit 20bb176

Browse files
committed
Initial commit
1 parent 4a6a992 commit 20bb176

File tree

12 files changed

+389
-0
lines changed

12 files changed

+389
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Alexey
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,166 @@
11
# pytest-evm
2+
3+
[![Croco Logo](https://i.ibb.co/G5Pjt6M/logo.png)](https://t.me/crocofactory)
4+
25
The testing package containing tools to test Web3-based projects
6+
7+
- **[Telegram channel](https://t.me/crocofactory)**
8+
- **[Bug reports](https://github.com/blnkoff/pytest-evm/issues)**
9+
10+
Package's source code is made available under the [MIT License](LICENSE)
11+
12+
# Quick Start
13+
There are few features simplifying your testing with pytest:
14+
- **[Fixtures](#fixtures)**
15+
- **[Test Reporting](#test-reporting)**
16+
- **[Usage Example](#usage-example)**
17+
18+
## Fixtures
19+
20+
### make_wallet
21+
This fixture simplify creating wallet instances as fixtures. Wallet instances are from `evm-wallet` package
22+
23+
```python
24+
import os
25+
import pytest
26+
from typing import Optional
27+
from evm_wallet.types import NetworkOrInfo
28+
from evm_wallet import AsyncWallet, Wallet
29+
30+
@pytest.fixture(scope="session")
31+
def make_wallet():
32+
def _make_wallet(network: NetworkOrInfo, private_key: Optional[str] = None, is_async: bool = True):
33+
if not private_key:
34+
private_key = os.getenv('TEST_PRIVATE_KEY')
35+
return AsyncWallet(private_key, network) if is_async else Wallet(private_key, network)
36+
37+
return _make_wallet
38+
```
39+
40+
You can specify whether your wallet should be of async or sync version. Instead of specifying RPC, you only have to provide
41+
chain's name. You can also specify a custom Network, using `NetworkOrInfo`.
42+
43+
```python
44+
import pytest
45+
46+
@pytest.fixture
47+
def wallet(make_wallet):
48+
return make_wallet('Optimism')
49+
```
50+
51+
As you can see, a private key wasn't passed. This because of by-default `make_wallet` takes it from
52+
environment variable `TEST_PRIVATE_KEY`. You can set environment variables using extra-package `python-dotenv`.
53+
54+
```python
55+
# conftest.py
56+
57+
import pytest
58+
from dotenv import load_dotenv
59+
60+
load_dotenv()
61+
62+
63+
@pytest.fixture(scope="session")
64+
def wallet(make_wallet):
65+
return make_wallet('Polygon')
66+
```
67+
68+
Here is the content of .env file
69+
70+
```shell
71+
# .env
72+
73+
TEST_PRIVATE_KEY=0x0000000000000000000000000000000000000000
74+
```
75+
76+
You can install `python-dotenv` along with `pytest-evm`:
77+
78+
```shell
79+
pip install pytest-evm[dotenv]
80+
```
81+
82+
### zero_address
83+
This fixture returns ZERO_ADDRESS value
84+
85+
```python
86+
import pytest
87+
from evm_wallet import ZERO_ADDRESS
88+
89+
@pytest.fixture(scope="session")
90+
def zero_address():
91+
return ZERO_ADDRESS
92+
```
93+
94+
### eth_amount
95+
This fixture returns 0.001 ETH in Wei, which is the most using minimal value for tests
96+
97+
```python
98+
import pytest
99+
from web3 import AsyncWeb3
100+
101+
@pytest.fixture(scope="session")
102+
def eth_amount():
103+
amount = AsyncWeb3.to_wei(0.001, 'ether')
104+
return amount
105+
```
106+
107+
## Test Reporting
108+
If your test performs one transaction, you can automatically `assert` transaction status and get useful report after test,
109+
if it completed successfully. To do this, you need to add mark `pytest.mark.tx` to your test.
110+
111+
```python
112+
import pytest
113+
114+
@pytest.mark.tx
115+
@pytest.mark.asyncio
116+
async def test_transaction(wallet, eth_amount):
117+
recipient = '0xe977Fa8D8AE7D3D6e28c17A868EF04bD301c583f'
118+
params = await wallet.build_transaction_params(eth_amount, recipient=recipient)
119+
return await wallet.transact(params)
120+
```
121+
122+
After test, you get similar report:
123+
124+
![Test Report](https://i.ibb.co/n8vKXwB/Screenshot-2024-01-24-at-22-08-29.png)
125+
126+
## Usage Example
127+
Here is example of testing with `pytest-evm`:
128+
129+
```python
130+
import pytest
131+
132+
class TestBridge:
133+
@pytest.mark.tx
134+
@pytest.mark.asyncio
135+
async def test_swap(self, wallet, eth_amount, bridge, destination_network):
136+
return await bridge.swap(eth_amount, destination_network)
137+
138+
@pytest.mark.tx
139+
@pytest.mark.asyncio
140+
async def test_swap_to_eth(self, wallet, eth_amount, bridge):
141+
return await bridge.swap_to_eth(eth_amount)
142+
143+
@pytest.fixture
144+
def wallet(self, make_wallet):
145+
return make_wallet('Optimism')
146+
147+
@pytest.fixture
148+
def bridge(self, wallet):
149+
return Bridge(wallet)
150+
151+
@pytest.fixture
152+
def destination_network(self):
153+
return 'Arbitrum'
154+
```
155+
156+
# Installing pytest-evm
157+
To install the package from GitHub you can use:
158+
159+
```shell
160+
pip install git+https://github.com/blnkoff/pytest-evm.git
161+
```
162+
163+
To install the package from PyPi you can use:
164+
```shell
165+
pip install pytest-evm
166+
```

pyproject.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[tool.poetry]
2+
name = 'pytest_evm'
3+
version = '0.1.0'
4+
description = 'The testing package containing tools to test Web3-based projects'
5+
authors = ['Alexey <[email protected]>']
6+
license = 'MIT'
7+
readme = 'README.md'
8+
repository = 'https://github.com/blnkoff/pytest-evm'
9+
homepage = 'https://github.com/blnkoff/pytest-evm'
10+
classifiers = [
11+
'Development Status :: 4 - Beta',
12+
'Intended Audience :: Developers',
13+
'Topic :: Software Development :: Libraries :: Python Module',
14+
'Topic :: Software Development :: Testing',
15+
'Framework :: Pytest',
16+
'Programming Language :: Python :: 3.11',
17+
'Programming Language :: Python :: 3 :: Only',
18+
'License :: OSI Approved :: MIT License',
19+
'Operating System :: Microsoft :: Windows',
20+
'Operating System :: MacOS'
21+
]
22+
packages = [{ include = 'pytest_evm' }]
23+
24+
[tool.poetry.dependencies]
25+
python = '^3.11'
26+
web3 = "^6.12.0"
27+
pytest = "^7.4.3"
28+
pytest-asyncio = "^0.23.2"
29+
evm-wallet = "^1.1.1"
30+
python-dotenv = {version = "^1.0.1", optional = true}
31+
32+
[tool.poetry.extras]
33+
dotenv = ["python-dotenv"]
34+
35+
[build-system]
36+
requires = ['poetry-core']
37+
build-backend = 'poetry.core.masonry.api'
38+
39+
[tool.poetry.plugins."pytest11"]
40+
evm_fixtures = 'pytest_evm.fixtures'
41+
evm_hooks = 'pytest_evm.hooks'
42+
43+
[project.entry-points."timmins.display"]
44+
evm_fixtures = 'pytest_evm.fixtures'
45+
evm_hooks = 'pytest_evm.hooks'

pytest_evm/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
pytest-evm
3+
~~~~~~~~~~~~~~
4+
The testing package containing tools to test Web3-based projects
5+
6+
Usage example:
7+
8+
:copyright: (c) 2023 by Alexey
9+
:license: MIT, see LICENSE for more details.
10+
"""

pytest_evm/fixtures.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import os
2+
import pytest
3+
from typing import Optional
4+
from evm_wallet import ZERO_ADDRESS
5+
from evm_wallet.types import NetworkOrInfo
6+
from evm_wallet import AsyncWallet, Wallet
7+
from web3 import AsyncWeb3
8+
9+
10+
@pytest.fixture(scope="session")
11+
def make_wallet():
12+
def _make_wallet(network: NetworkOrInfo, private_key: Optional[str] = None, is_async: bool = True):
13+
if not private_key:
14+
private_key = os.getenv('TEST_PRIVATE_KEY')
15+
return AsyncWallet(private_key, network) if is_async else Wallet(private_key, network)
16+
17+
return _make_wallet
18+
19+
20+
@pytest.fixture(scope="session")
21+
def eth_amount():
22+
amount = AsyncWeb3.to_wei(0.001, 'ether')
23+
return amount
24+
25+
26+
@pytest.fixture(scope="session")
27+
def zero_address():
28+
return ZERO_ADDRESS

pytest_evm/hooks.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pytest
2+
from web3 import Web3
3+
from functools import wraps
4+
from typing import Iterable
5+
from evm_wallet import Wallet
6+
from pytest_evm.utils import validate_status, get_last_transaction, get_balance
7+
8+
9+
def pytest_configure(config):
10+
config.addinivalue_line("markers", "tx: mark a test as a transaction test.")
11+
12+
13+
def pytest_collection_modifyitems(items: Iterable[pytest.Item]):
14+
for item in items:
15+
if item.get_closest_marker("tx"):
16+
original_test_function = item.obj
17+
18+
@validate_status
19+
@wraps(original_test_function)
20+
async def wrapped_test_function(*args, **kwargs):
21+
return await original_test_function(*args, **kwargs)
22+
23+
item.obj = wrapped_test_function
24+
25+
26+
def pytest_runtest_makereport(item: pytest.Item, call):
27+
if call.when == 'call':
28+
if call.excinfo is not None:
29+
pass
30+
else:
31+
if item.get_closest_marker("tx"):
32+
try:
33+
wallet = item.funcargs['wallet']
34+
wallet = Wallet(wallet.private_key, wallet.network)
35+
tx = get_last_transaction(wallet)
36+
last_tx_hash = tx['hash'].hex()
37+
balance = get_balance(wallet)
38+
costs = tx['value'] + tx['gas']*tx['gasPrice']
39+
costs = Web3.from_wei(costs, 'ether')
40+
print()
41+
print(f'From: https://goerli.etherscan.io/address/{wallet.public_key}')
42+
print(f'Transaction: {wallet.get_explorer_url(last_tx_hash)}')
43+
print(f'Costs: {costs} {wallet.network["token"]}')
44+
print(f'Balance: {balance} {wallet.network["token"]}')
45+
print(f'Network: {wallet.network["network"]}')
46+
except Exception as ex:
47+
print("There was an error while getting information about transaction")
48+

pytest_evm/pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
markers =
3+
tx: mark a test as a transaction test.

pytest_evm/utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from functools import wraps
2+
from evm_wallet import Wallet
3+
from web3 import Web3
4+
from web3.middleware import geth_poa_middleware
5+
6+
7+
def validate_status(func):
8+
@wraps(func)
9+
async def wrapper(*args, **kwargs):
10+
wallet = kwargs['wallet']
11+
tx_hash = await func(*args, **kwargs)
12+
status = bool(await wallet.provider.eth.wait_for_transaction_receipt(tx_hash))
13+
assert status
14+
15+
return wrapper
16+
17+
18+
def get_last_transaction(wallet: Wallet):
19+
w3 = Web3(Web3.HTTPProvider(wallet.network['rpc']))
20+
w3.middleware_onion.inject(geth_poa_middleware, layer=0)
21+
22+
block_number = w3.eth.block_number
23+
start_block = 0
24+
end_block = block_number
25+
26+
last_transaction = None
27+
wallet_address = wallet.public_key
28+
29+
for block in range(end_block, start_block - 1, -1):
30+
block_info = w3.eth.get_block(block, True)
31+
32+
for tx in reversed(block_info['transactions']):
33+
if wallet_address.lower() in [tx['from'].lower(), tx['to'].lower()]:
34+
last_transaction = tx
35+
break
36+
37+
if last_transaction:
38+
break
39+
40+
return last_transaction
41+
42+
43+
def get_balance(wallet: Wallet) -> int:
44+
w3 = Web3(Web3.HTTPProvider(wallet.network['rpc']))
45+
balance = w3.eth.get_balance(account=wallet.public_key)
46+
balance = w3.from_wei(balance, 'ether')
47+
return balance

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import pytest
2+
from dotenv import load_dotenv
3+
4+
load_dotenv()
5+
6+
7+
@pytest.fixture(scope="session")
8+
def wallet(make_wallet):
9+
return make_wallet('Polygon')

0 commit comments

Comments
 (0)