-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
389 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[pytest] | ||
markers = | ||
tx: mark a test as a transaction test. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.