Testing guide¶
The suite has two layers: fast, network-free unit tests that run by default, and integration tests against a real local Hardhat node that are opted into explicitly.
Running the tests¶
# Unit tests (network- and integration-marked tests are skipped)
make test # == uv run pytest
# Hardhat integration suite
make test-integration
# Lint + format check + mypy + unit tests
make check
Individual gates:
make lint # uv run ruff check .
make format-check # uv run ruff format --check .
make type-check # uv run mypy --strict blockchainpype/
mypy runs in strict mode over the package (tests are excluded via
[tool.mypy] exclude), and CI additionally enforces --cov-fail-under=80.
Test selection and markers¶
Two markers are declared in pyproject.toml:
| Marker | Meaning |
|---|---|
network |
Needs live network access (real RPC endpoints) |
integration |
Needs an external service (a Hardhat node, npm toolchain) |
The default selection comes from addopts, so plain pytest never touches the
network:
addopts = "-m 'not network and not integration'"
Override it on the command line to widen the selection:
uv run pytest -m "" # everything, including integration
uv run pytest -m integration # only the integration tests
uv run pytest tests/evm/test_erc20.py -k allowance
Other relevant pytest settings: asyncio_mode = "auto" (async tests need no
decorator), asyncio_default_fixture_loop_scope = "function" and a global
30-second timeout.
Unit-test philosophy¶
Mock only at the RPC/HTTP boundary. Everything above it — the blockchain operator, wallet, contract, strategy and facade — is the real code under test. Concretely:
- No production method is ever patched. Tests never replace the function they are meant to verify with a stub that returns the expected answer.
- Fake transports return realistic payloads: ABI-encoded
eth_callresults, complete post-London block and receipt objects, real protocol JSON. That keeps the decoding layer (web3 ABI decoding, tuple index maps, borsh parsing) inside the test's coverage. - Assertions use exact values — the precise
Decimal, the exact calldata selector and arguments, the exact number of raw units — instead of loose "is not None" checks.
The fake JSON-RPC provider (EVM)¶
tests/evm/test_wallet.py defines a FakeRPCProvider, an
AsyncJSONBaseProvider that answers from a response table. Values can be a
plain result, a callable receiving the request params, an Exception to raise,
or a dict containing an error key (returned as a JSON-RPC error response).
Every call is recorded in provider.calls, and calls_for(method) retrieves
the params a method was invoked with. An unexpected method raises immediately,
so a test can never silently exercise an unstubbed code path.
Building a blockchain on top of it is exactly the production path, with the provider swapped:
from financepype.platforms.blockchain import BlockchainPlatform
from blockchainpype.evm.blockchain.blockchain import (
EthereumBlockchain,
EthereumBlockchainType,
)
from blockchainpype.evm.blockchain.configuration import (
EthereumBlockchainConfiguration,
EthereumConnectivityConfiguration,
EthereumNativeAssetConfiguration,
)
from tests.evm.test_wallet import FakeRPCProvider
def build_blockchain() -> EthereumBlockchain:
provider = FakeRPCProvider(
{
"eth_chainId": "0x1",
"eth_getBalance": "0xde0b6b3a7640000", # exactly 1 ETH
"eth_getTransactionCount": "0x2",
}
)
return EthereumBlockchain(
configuration=EthereumBlockchainConfiguration(
platform=BlockchainPlatform(
identifier="ethereum",
type=EthereumBlockchainType,
chain_id=1,
),
native_asset=EthereumNativeAssetConfiguration(),
connectivity=EthereumConnectivityConfiguration(rpc_provider=provider),
explorer=None,
)
)
Contract-level tests answer eth_call with a handler that dispatches on the
4-byte selector and encodes the reply with eth_abi.encode, so the real ABI
decoder runs:
from typing import Any
from eth_abi import encode
SEL_DECIMALS = "0x313ce567"
SEL_BALANCE_OF = "0x70a08231"
def eth_call_handler(params: Any) -> str:
call = params[0]
data = str(call.get("data") or call.get("input"))
selector = data[:10].lower()
if selector == SEL_DECIMALS:
return "0x" + encode(["uint8"], [6]).hex()
if selector == SEL_BALANCE_OF:
return "0x" + encode(["uint256"], [1_500_000]).hex()
raise AssertionError(f"Unexpected eth_call selector: {selector}")
Fake HTTP sessions¶
The Polymarket tests apply the same idea one layer up: a FakeHttpSession
answers (method, path) pairs with recorded CLOB / Gamma / Data-API payloads,
and is injected through the strategy's dependency-injection hooks
(ClobClient(session=...), Polymarket(..., clob_client=..., session=...))
rather than by patching aiohttp.
Background tasks¶
sign_and_send_transaction schedules the broadcast as a background task, so
tests await a drain helper before asserting on the broadcast outcome — the
wallet keeps strong references to its in-flight tasks, which makes that
deterministic.
Solana and dapp-layer tests¶
Solana tests stub the AsyncClient responses (account data, token balances,
signature statuses) and let the real binary parsers run over byte payloads laid
out exactly like the on-chain structs. The abstract dapp tests in
tests/dapps/ build real financepype assets through
tests/dapps/helpers.py and register a blockchain for the test platform, so
the facades exercise their real dispatch logic.
Hardhat integration environment¶
One-time setup¶
The harness needs Node.js (CI uses Node 22) and the npm dependencies:
cd common/hardhat
npm install
Then run the suite from the repository root:
make test-integration
# == uv run pytest tests/evm/test_hardhat.py -m "" --timeout=300
-m "" clears the default marker filter; --timeout raises the 30 s global
limit, since starting a node and deploying contracts takes longer.
What the harness does¶
tests/evm/hardhat.py provides HardhatNode and HardhatTestEnvironment:
- Checks the Node.js version, then starts
npx hardhat nodeon a free port allocated dynamically, so concurrent test sessions never clash on 8545. - Deploys the test contracts (
scripts/deploy-all.js), writing the addresses to a session-private temporary file rather than the shareddeployments.json. - Registers a
hardhatplatform in theBlockchainFactorypointing at that port — the library default hardcodes 8545, so the environment registers aBlockchainConfigurationssubclass overriding justhardhat_configuration(), after dropping any stale registration. - Reads the deterministic test accounts (20 accounts funded with 10 000 ETH, chain id 31337).
- On teardown, disconnects the provider and unregisters the platform so a later environment starts clean.
Fixtures¶
| Fixture | Scope | What you get |
|---|---|---|
hardhat_dir |
session | Path to common/hardhat |
hardhat_env |
session (session loop) | The running HardhatTestEnvironment |
blockchain |
function (session loop) | The EthereumBlockchain bound to the node |
test_accounts |
function (session loop) | The funded account addresses |
deployed_contracts |
function (session loop) | Contract name → deployed address |
blockchain_snapshot |
function (session loop) | Snapshots before the test and reverts after |
HardhatTestEnvironment also exposes get_account_balance_wei,
get_account_balance, send_eth, mine_blocks,
set_next_block_timestamp, snapshot, revert_to_snapshot and
reset_node; get_hardhat_accounts() / get_hardhat_private_keys() return
the deterministic account list and its keys for signing.
The session event loop¶
All async fixtures run in the session-scoped event loop
(loop_scope="session"), so the node, the provider and web3's cached aiohttp
session all live on one loop. Tests using them must opt into the same loop, or
pytest-asyncio will run them on a fresh function-scoped loop and the awaits
will fail:
import pytest
pytestmark = [pytest.mark.integration, pytest.mark.timeout(120)]
class TestSomething:
pytestmark = pytest.mark.asyncio(loop_scope="session")
async def test_block_number(self, blockchain):
assert await blockchain.fetch_block_number() >= 0
async def test_transfer(self, hardhat_env, test_accounts):
sender, receiver = test_accounts[0], test_accounts[1]
before = await hardhat_env.get_account_balance_wei(receiver)
await hardhat_env.send_eth(sender, receiver, 1.0)
after = await hardhat_env.get_account_balance_wei(receiver)
assert after == before + 10**18 # exact, in wei
Two integration modules ship with the library:
tests/evm/test_hardhat.py (node lifecycle, ETH transfers, mining, snapshots,
contract deployment and a real token swap through SimpleV2Router) and
tests/evm/test_uniswap_hardhat_integration.py (the whole stack end-to-end:
wallet lifecycle, ERC20Contract against the deployed TestToken, the
UniswapV2 strategy against the deployed factory + router, and the
UniswapDEX facade configured for the local network). The latter cross-checks
its quotes both against the canonical constant-product formula and against the
router's own getAmountsOut/getAmountsIn, and state-changing tests take the
blockchain_snapshot fixture so the seeded deployment state is restored
afterwards. Both modules are marked integration and skipped by the default
run:
uv run pytest tests/evm/test_uniswap_hardhat_integration.py -m "" --timeout=300
Forked networks¶
The Hardhat configuration also supports forking mainnet or an L2 (set
INFURA_API_KEY/ALCHEMY_API_KEY and FORK_CHAIN, or a chain-specific
*_FORK_URL), which lets integration tests run against real protocol
deployments. See common/hardhat/README.md and
common/hardhat/MULTI_CHAIN_FORKING.md.
Testing your own code¶
The same building blocks work for applications built on the library:
- Construct blockchains and wallets explicitly
(
EthereumWallet(configuration=..., blockchain=...),UniswapDEX(blockchain, configuration, wallet=wallet)) so no global registry state leaks between tests.BlockchainFactory.reset(),WalletFactory.reset()andWalletRegistry.reset()clear the registries when you do rely on them. - Swap in a fake provider or a fake HTTP session at the transport boundary and keep the rest of the stack real.
- Mark anything that reaches a live endpoint with
network, and anything needing a node or another service withintegration, so the default run stays fast and offline.
See also¶
- EVM guide and Solana guide — the APIs under test.
- DApps guide — protocol strategies and their build-only contract.