Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
blnkoff committed Jan 24, 2024
1 parent 4a6a992 commit 20bb176
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Alexey

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,166 @@
# pytest-evm

[![Croco Logo](https://i.ibb.co/G5Pjt6M/logo.png)](https://t.me/crocofactory)

The testing package containing tools to test Web3-based projects

- **[Telegram channel](https://t.me/crocofactory)**
- **[Bug reports](https://github.com/blnkoff/pytest-evm/issues)**

Package's source code is made available under the [MIT License](LICENSE)

# Quick Start
There are few features simplifying your testing with pytest:
- **[Fixtures](#fixtures)**
- **[Test Reporting](#test-reporting)**
- **[Usage Example](#usage-example)**

## Fixtures

### make_wallet
This fixture simplify creating wallet instances as fixtures. Wallet instances are from `evm-wallet` package

```python
import os
import pytest
from typing import Optional
from evm_wallet.types import NetworkOrInfo
from evm_wallet import AsyncWallet, Wallet

@pytest.fixture(scope="session")
def make_wallet():
def _make_wallet(network: NetworkOrInfo, private_key: Optional[str] = None, is_async: bool = True):
if not private_key:
private_key = os.getenv('TEST_PRIVATE_KEY')
return AsyncWallet(private_key, network) if is_async else Wallet(private_key, network)

return _make_wallet
```

You can specify whether your wallet should be of async or sync version. Instead of specifying RPC, you only have to provide
chain's name. You can also specify a custom Network, using `NetworkOrInfo`.

```python
import pytest

@pytest.fixture
def wallet(make_wallet):
return make_wallet('Optimism')
```

As you can see, a private key wasn't passed. This because of by-default `make_wallet` takes it from
environment variable `TEST_PRIVATE_KEY`. You can set environment variables using extra-package `python-dotenv`.

```python
# conftest.py

import pytest
from dotenv import load_dotenv

load_dotenv()


@pytest.fixture(scope="session")
def wallet(make_wallet):
return make_wallet('Polygon')
```

Here is the content of .env file

```shell
# .env

TEST_PRIVATE_KEY=0x0000000000000000000000000000000000000000
```

You can install `python-dotenv` along with `pytest-evm`:

```shell
pip install pytest-evm[dotenv]
```

### zero_address
This fixture returns ZERO_ADDRESS value

```python
import pytest
from evm_wallet import ZERO_ADDRESS

@pytest.fixture(scope="session")
def zero_address():
return ZERO_ADDRESS
```

### eth_amount
This fixture returns 0.001 ETH in Wei, which is the most using minimal value for tests

```python
import pytest
from web3 import AsyncWeb3

@pytest.fixture(scope="session")
def eth_amount():
amount = AsyncWeb3.to_wei(0.001, 'ether')
return amount
```

## Test Reporting
If your test performs one transaction, you can automatically `assert` transaction status and get useful report after test,
if it completed successfully. To do this, you need to add mark `pytest.mark.tx` to your test.

```python
import pytest

@pytest.mark.tx
@pytest.mark.asyncio
async def test_transaction(wallet, eth_amount):
recipient = '0xe977Fa8D8AE7D3D6e28c17A868EF04bD301c583f'
params = await wallet.build_transaction_params(eth_amount, recipient=recipient)
return await wallet.transact(params)
```

After test, you get similar report:

![Test Report](https://i.ibb.co/n8vKXwB/Screenshot-2024-01-24-at-22-08-29.png)

## Usage Example
Here is example of testing with `pytest-evm`:

```python
import pytest

class TestBridge:
@pytest.mark.tx
@pytest.mark.asyncio
async def test_swap(self, wallet, eth_amount, bridge, destination_network):
return await bridge.swap(eth_amount, destination_network)

@pytest.mark.tx
@pytest.mark.asyncio
async def test_swap_to_eth(self, wallet, eth_amount, bridge):
return await bridge.swap_to_eth(eth_amount)

@pytest.fixture
def wallet(self, make_wallet):
return make_wallet('Optimism')

@pytest.fixture
def bridge(self, wallet):
return Bridge(wallet)

@pytest.fixture
def destination_network(self):
return 'Arbitrum'
```

# Installing pytest-evm
To install the package from GitHub you can use:

```shell
pip install git+https://github.com/blnkoff/pytest-evm.git
```

To install the package from PyPi you can use:
```shell
pip install pytest-evm
```
45 changes: 45 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[tool.poetry]
name = 'pytest_evm'
version = '0.1.0'
description = 'The testing package containing tools to test Web3-based projects'
authors = ['Alexey <abelenkov2006@gmail.com>']
license = 'MIT'
readme = 'README.md'
repository = 'https://github.com/blnkoff/pytest-evm'
homepage = 'https://github.com/blnkoff/pytest-evm'
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Module',
'Topic :: Software Development :: Testing',
'Framework :: Pytest',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3 :: Only',
'License :: OSI Approved :: MIT License',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS'
]
packages = [{ include = 'pytest_evm' }]

[tool.poetry.dependencies]
python = '^3.11'
web3 = "^6.12.0"
pytest = "^7.4.3"
pytest-asyncio = "^0.23.2"
evm-wallet = "^1.1.1"
python-dotenv = {version = "^1.0.1", optional = true}

[tool.poetry.extras]
dotenv = ["python-dotenv"]

[build-system]
requires = ['poetry-core']
build-backend = 'poetry.core.masonry.api'

[tool.poetry.plugins."pytest11"]
evm_fixtures = 'pytest_evm.fixtures'
evm_hooks = 'pytest_evm.hooks'

[project.entry-points."timmins.display"]
evm_fixtures = 'pytest_evm.fixtures'
evm_hooks = 'pytest_evm.hooks'
10 changes: 10 additions & 0 deletions pytest_evm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
pytest-evm
~~~~~~~~~~~~~~
The testing package containing tools to test Web3-based projects
Usage example:
:copyright: (c) 2023 by Alexey
:license: MIT, see LICENSE for more details.
"""
28 changes: 28 additions & 0 deletions pytest_evm/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import pytest
from typing import Optional
from evm_wallet import ZERO_ADDRESS
from evm_wallet.types import NetworkOrInfo
from evm_wallet import AsyncWallet, Wallet
from web3 import AsyncWeb3


@pytest.fixture(scope="session")
def make_wallet():
def _make_wallet(network: NetworkOrInfo, private_key: Optional[str] = None, is_async: bool = True):
if not private_key:
private_key = os.getenv('TEST_PRIVATE_KEY')
return AsyncWallet(private_key, network) if is_async else Wallet(private_key, network)

return _make_wallet


@pytest.fixture(scope="session")
def eth_amount():
amount = AsyncWeb3.to_wei(0.001, 'ether')
return amount


@pytest.fixture(scope="session")
def zero_address():
return ZERO_ADDRESS
48 changes: 48 additions & 0 deletions pytest_evm/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import pytest
from web3 import Web3
from functools import wraps
from typing import Iterable
from evm_wallet import Wallet
from pytest_evm.utils import validate_status, get_last_transaction, get_balance


def pytest_configure(config):
config.addinivalue_line("markers", "tx: mark a test as a transaction test.")


def pytest_collection_modifyitems(items: Iterable[pytest.Item]):
for item in items:
if item.get_closest_marker("tx"):
original_test_function = item.obj

@validate_status
@wraps(original_test_function)
async def wrapped_test_function(*args, **kwargs):
return await original_test_function(*args, **kwargs)

item.obj = wrapped_test_function


def pytest_runtest_makereport(item: pytest.Item, call):
if call.when == 'call':
if call.excinfo is not None:
pass
else:
if item.get_closest_marker("tx"):
try:
wallet = item.funcargs['wallet']
wallet = Wallet(wallet.private_key, wallet.network)
tx = get_last_transaction(wallet)
last_tx_hash = tx['hash'].hex()
balance = get_balance(wallet)
costs = tx['value'] + tx['gas']*tx['gasPrice']
costs = Web3.from_wei(costs, 'ether')
print()
print(f'From: https://goerli.etherscan.io/address/{wallet.public_key}')
print(f'Transaction: {wallet.get_explorer_url(last_tx_hash)}')
print(f'Costs: {costs} {wallet.network["token"]}')
print(f'Balance: {balance} {wallet.network["token"]}')
print(f'Network: {wallet.network["network"]}')
except Exception as ex:
print("There was an error while getting information about transaction")

3 changes: 3 additions & 0 deletions pytest_evm/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
markers =
tx: mark a test as a transaction test.
47 changes: 47 additions & 0 deletions pytest_evm/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from functools import wraps
from evm_wallet import Wallet
from web3 import Web3
from web3.middleware import geth_poa_middleware


def validate_status(func):
@wraps(func)
async def wrapper(*args, **kwargs):
wallet = kwargs['wallet']
tx_hash = await func(*args, **kwargs)
status = bool(await wallet.provider.eth.wait_for_transaction_receipt(tx_hash))
assert status

return wrapper


def get_last_transaction(wallet: Wallet):
w3 = Web3(Web3.HTTPProvider(wallet.network['rpc']))
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

block_number = w3.eth.block_number
start_block = 0
end_block = block_number

last_transaction = None
wallet_address = wallet.public_key

for block in range(end_block, start_block - 1, -1):
block_info = w3.eth.get_block(block, True)

for tx in reversed(block_info['transactions']):
if wallet_address.lower() in [tx['from'].lower(), tx['to'].lower()]:
last_transaction = tx
break

if last_transaction:
break

return last_transaction


def get_balance(wallet: Wallet) -> int:
w3 = Web3(Web3.HTTPProvider(wallet.network['rpc']))
balance = w3.eth.get_balance(account=wallet.public_key)
balance = w3.from_wei(balance, 'ether')
return balance
Empty file added tests/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pytest
from dotenv import load_dotenv

load_dotenv()


@pytest.fixture(scope="session")
def wallet(make_wallet):
return make_wallet('Polygon')
5 changes: 5 additions & 0 deletions tests/test_fixtutes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from evm_wallet import AsyncWallet


def test_make_wallet(make_wallet):
assert isinstance(make_wallet('Ethereum'), AsyncWallet)
Loading

0 comments on commit 20bb176

Please sign in to comment.